this repo has no description
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Set up scaffolding

+3109
+2
.claudeignore
··· 1 + target/ 2 + Cargo.lock
+3
.gitignore
··· 1 + /target 2 + node_modules/ 3 + settings.local.json
+188
CLAUDE.md
··· 1 + # CLAUDE.md — Opake 2 + 3 + **opake.app** — An encrypted personal cloud built on the AT Protocol. 4 + 5 + ## Project Overview 6 + 7 + Opake uses a self-hosted PDS as the storage and identity layer, with custom lexicons for file management, encryption, and sharing. The encryption model follows the same hybrid pattern as git-crypt: content is encrypted with per-document symmetric keys (AES-256-GCM), and those keys are wrapped (asymmetrically encrypted) to authorized DIDs' public keys. 8 + 9 + The PDS doesn't need modification — it stores encrypted blobs as opaque bytes and encryption metadata as standard atproto records. All crypto happens client-side. The name comes from the Dutch-flavored spelling of "opaque" — because that's exactly what your data is to everyone without the key. 10 + 11 + ## Core Concepts 12 + 13 + ### Why atproto? 14 + 15 + A PDS is essentially cloud storage with an API. It stores signed, schema-validated records in a Merkle Search Tree, plus binary blobs. It doesn't understand or inspect the data — it just manages it. We define custom lexicons under `app.opake.cloud.*` to give structure to our files, encryption metadata, and sharing grants. The PDS handles identity (DID-based), authentication, blob storage (up to 50MB default), federation, and sync — all for free. 16 + 17 + ### Encryption Model 18 + 19 + Every file is encrypted before upload. The PDS and the network only ever see ciphertext. 20 + 21 + ``` 22 + Plaintext file 23 + → encrypt with random AES-256-GCM key K → ciphertext blob (uploaded to PDS) 24 + → wrap K with owner's DID public key → stored in document record 25 + → to share: wrap K with recipient's DID public key → stored in grant record 26 + ``` 27 + 28 + There are two sharing modes: 29 + 30 + **Direct encryption** — the content key is wrapped individually to each authorized DID. Good for ad-hoc sharing of individual files. 31 + 32 + **Keyring encryption** — a named group has a shared group key (GK), wrapped to each member's DID. Individual documents have their content key wrapped under GK. Adding a member to the keyring gives them access to all documents under it without per-document changes. This is the git-crypt named-key equivalent. 33 + 34 + ### Data Stays Put 35 + 36 + When sharing a file with a user on another PDS, no data is copied. The recipient's client fetches the document record and blob directly from the owner's PDS via standard atproto APIs (`com.atproto.repo.getRecord`, `com.atproto.sync.getBlob`). The owner remains the single source of truth. Revocation means deleting the grant record (and optionally re-encrypting with a new key). 37 + 38 + ### Plaintext Metadata Tradeoff 39 + 40 + File names, tags, MIME types, and descriptions are intentionally stored unencrypted in the document record. This allows a personal AppView to index and search files server-side without access to encryption keys. If full opacity is needed, these fields can be set to dummy values with real metadata stored inside the encrypted blob — the schema supports both approaches. 41 + 42 + ## Lexicon Schema 43 + 44 + All lexicons live under the `app.opake.cloud.*` namespace (owner controls the `opake.app` domain for NSID authority). 45 + 46 + ### `app.opake.cloud.defs` 47 + Shared type definitions: 48 + - **wrappedKey** — a symmetric key encrypted to a specific DID's public key. Fields: `did`, `ciphertext` (bytes), `algo` (e.g. `ECDH-ES+A256KW`). 49 + - **encryptionEnvelope** — describes content encryption: `algo` (e.g. `aes-256-gcm`), `nonce` (bytes), and `keys` (array of wrappedKey). 50 + - **keyringRef** — reference to a keyring record plus the content key wrapped under the group key. 51 + - **visibility** — hint string: `private`, `shared`, or `public`. 52 + 53 + ### `app.opake.cloud.document` 54 + The core file record. Key type: `tid`. 55 + - `name` (string) — plaintext filename 56 + - `mimeType` (string) — original MIME type of unencrypted content 57 + - `size` (integer) — original unencrypted size in bytes 58 + - `blob` (blob) — the encrypted file content, uploaded as `application/octet-stream` 59 + - `encryption` (union) — either `directEncryption` (inline envelope with wrapped keys) or `keyringEncryption` (reference to a keyring + wrapped content key) 60 + - `tags` (array of strings) — plaintext tags for search/categorization 61 + - `parent` (at-uri, optional) — reference to parent document for folder hierarchy 62 + - `visibility`, `description`, `createdAt`, `modifiedAt` 63 + 64 + ### `app.opake.cloud.keyring` 65 + A named group for shared access. Key type: `tid`. 66 + - `name` (string) — human-readable group name (e.g. "family-photos") 67 + - `algo` (string) — symmetric algorithm the group key targets 68 + - `members` (array of wrappedKey) — the group key wrapped to each member's DID 69 + - `rotation` (integer) — incremented on key rotation after member removal 70 + - `createdAt`, `modifiedAt` 71 + 72 + ### `app.opake.cloud.grant` 73 + An ad-hoc share grant. Key type: `tid`. 74 + - `document` (at-uri) — the document being shared 75 + - `recipient` (did) — who gets access 76 + - `wrappedKey` (wrappedKey) — the document's content key wrapped to the recipient 77 + - `permissions` (string) — advisory: `read` or `read-write` 78 + - `expiresAt` (datetime, optional) — advisory expiration 79 + - `note` (string, optional) — message to recipient 80 + - `createdAt` 81 + 82 + ## Architecture 83 + 84 + ``` 85 + ┌──────────────────────────┐ 86 + │ opake CLI (Rust) │ ← this is the project 87 + │ - encrypt/decrypt files │ 88 + │ - key management │ 89 + │ - DID key resolution │ 90 + │ - keyring/grant CRUD │ 91 + │ - upload/download blobs │ 92 + └──────────┬───────────────┘ 93 + │ XRPC (HTTPS) 94 + 95 + Your existing PDS ← external, already running 96 + (any implementation) 97 + 98 + ┌──────────────────────────┐ 99 + │ AppView + SPA (later) │ ← future phase 100 + │ - Rust/Axum JSON API │ 101 + │ - indexes metadata │ 102 + │ - TS or Yew frontend │ 103 + │ - client-side crypto │ 104 + └──────────────────────────┘ 105 + ``` 106 + 107 + The CLI talks directly to the PDS over XRPC. No middleware, no AppView needed for the core workflow. The AppView becomes relevant later when you want a web UI with search, file browsing, and "shared with me" views. 108 + 109 + ## Implementation Plan 110 + 111 + ### Phase 1: CLI Foundation 112 + - [ ] Project scaffold: Rust binary with clap, config file for PDS URL + credentials 113 + - [ ] Auth: create session via `com.atproto.server.createSession`, manage tokens 114 + - [ ] `upload <file>` — generate AES-256-GCM key, encrypt file, upload blob via `com.atproto.repo.uploadBlob`, create `app.opake.cloud.document` record 115 + - [ ] `download <at-uri>` — fetch document record, fetch blob via `com.atproto.sync.getBlob`, decrypt, write to disk 116 + - [ ] `ls` — list document records via `com.atproto.repo.listRecords` 117 + - [ ] `rm <at-uri>` — delete document record (and blob becomes orphaned/GC'd) 118 + - [ ] Local keystore for the user's own wrapped keys (so you can decrypt your own files) 119 + 120 + ### Phase 2: Sharing 121 + - [ ] `resolve <handle-or-did>` — resolve a DID, fetch DID document, extract public key 122 + - [ ] `share <at-uri> <did>` — wrap content key to recipient's pubkey, create grant record 123 + - [ ] `revoke <grant-at-uri>` — delete grant record 124 + - [ ] `shared` — list grants you've created 125 + - [ ] `inbox` — list grants where you are the recipient (queries your own PDS for grants pointing to your DID... or requires an AppView for cross-PDS discovery) 126 + 127 + ### Phase 3: Keyrings 128 + - [ ] `keyring create <name>` — generate group key, wrap to self, create keyring record 129 + - [ ] `keyring add-member <keyring> <did>` — wrap group key to new member's pubkey 130 + - [ ] `keyring remove-member <keyring> <did>` — rotate group key, re-wrap to remaining members 131 + - [ ] `keyring ls` — list keyrings 132 + - [ ] `upload <file> --keyring <name>` — encrypt under a keyring instead of direct keys 133 + 134 + ### Phase 4: Web UI (future) 135 + - [ ] Rust/Axum JSON API server (the AppView) that indexes document/grant/keyring records 136 + - [ ] SPA frontend (TypeScript or Yew) with client-side crypto via Web Crypto API 137 + - [ ] File browser, search, upload/download, grant management UI 138 + 139 + ### Phase 5: Stretch 140 + - [ ] Folder hierarchy via `parent` references 141 + - [ ] Versioning (new document records referencing previous versions) 142 + - [ ] Large file sidecar service if 50MB limit becomes a problem 143 + - [ ] Additional record types: notes, bookmarks, etc. under `app.opake.cloud.*` 144 + 145 + ## Technology 146 + 147 + - **Rust CLI** is the primary deliverable. All core functionality (encrypt, upload, download, decrypt, keyring/grant management) lives here. 148 + - The atproto Rust ecosystem exists: see `atproto-crates` on Tangled for identity, OAuth, XRPC, record handling. 149 + - **PDS is external.** The project is PDS-agnostic — it talks to whatever PDS you point it at via XRPC. A PDS is already running; it's not part of this project. 150 + - **Web UI is a later phase.** Either a TypeScript SPA or a Yew (Rust/WASM) app, TBD. The Rust AppView would be a JSON API server (Axum) that the SPA talks to. 151 + - **Infrastructure (Caddy, DNS, VPS) is already in place** and not part of this project. 152 + 153 + ## Key Design Decisions 154 + 155 + 1. **Encryption is client-side only.** The PDS never sees plaintext content. This means no server-side processing (thumbnails, previews, full-text search over encrypted content). Tradeoff accepted. 156 + 157 + 2. **Grants are separate records**, not inline in the document. This allows independent creation/deletion, efficient querying ("what's shared with me?"), and matches the atproto pattern of small, independent records. 158 + 159 + 3. **Two-layer key for keyrings.** Documents under a keyring still have their own per-document content key, wrapped under the group key. This means rotating the group key doesn't require re-encrypting every document's blob — only re-wrapping the group key to remaining members. 160 + 161 + 4. **No revocation guarantee for historical access.** Same limitation as git-crypt. If someone had the key and cached the blob, they can still read it. True revocation requires re-encrypting the blob with a new content key and deleting the old blob. The schema supports this workflow but doesn't enforce it. 162 + 163 + 5. **Plaintext metadata is opt-in transparency.** Names and tags are unencrypted by default for usability. Users who need full opacity can use dummy values and embed real metadata in the encrypted payload. 164 + 165 + 6. **50MB blob limit is fine for now.** Covers documents, photos, and short media. Large file support (video, archives) can come later via a sidecar service similar to how Tangled uses "knots" alongside the PDS. 166 + 167 + ## File Structure 168 + 169 + ``` 170 + lexicons/ 171 + ├── README.md # Architecture overview and flow diagrams 172 + ├── EXAMPLES.md # Concrete example records with annotations 173 + ├── app.opake.cloud.defs.json # Shared type definitions 174 + ├── app.opake.cloud.document.json # File/document record 175 + ├── app.opake.cloud.keyring.json # Group access keyring 176 + └── app.opake.cloud.grant.json # Ad-hoc share grant 177 + ``` 178 + 179 + ## References 180 + 181 + - AT Protocol specs: https://atproto.com/specs 182 + - Lexicon spec: https://atproto.com/specs/lexicon 183 + - PDS self-hosting: https://github.com/bluesky-social/pds 184 + - Custom schemas guide: https://docs.bsky.app/docs/advanced-guides/custom-schemas 185 + - Data model (blob format): https://atproto.com/specs/data-model 186 + - atproto Rust crates: https://tangled.org/ngerakines.me/atproto-crates 187 + - Tranquil PDS (Rust): mentioned in atproto self-hosting docs 188 + - Lexicon community registry: https://github.com/lexicon-community/awesome-lexicons
+1596
Cargo.lock
··· 1 + # This file is automatically @generated by Cargo. 2 + # It is not intended for manual editing. 3 + version = 4 4 + 5 + [[package]] 6 + name = "anstream" 7 + version = "0.6.21" 8 + source = "registry+https://github.com/rust-lang/crates.io-index" 9 + checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" 10 + dependencies = [ 11 + "anstyle", 12 + "anstyle-parse", 13 + "anstyle-query", 14 + "anstyle-wincon", 15 + "colorchoice", 16 + "is_terminal_polyfill", 17 + "utf8parse", 18 + ] 19 + 20 + [[package]] 21 + name = "anstyle" 22 + version = "1.0.13" 23 + source = "registry+https://github.com/rust-lang/crates.io-index" 24 + checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" 25 + 26 + [[package]] 27 + name = "anstyle-parse" 28 + version = "0.2.7" 29 + source = "registry+https://github.com/rust-lang/crates.io-index" 30 + checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" 31 + dependencies = [ 32 + "utf8parse", 33 + ] 34 + 35 + [[package]] 36 + name = "anstyle-query" 37 + version = "1.1.5" 38 + source = "registry+https://github.com/rust-lang/crates.io-index" 39 + checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" 40 + dependencies = [ 41 + "windows-sys 0.61.2", 42 + ] 43 + 44 + [[package]] 45 + name = "anstyle-wincon" 46 + version = "3.0.11" 47 + source = "registry+https://github.com/rust-lang/crates.io-index" 48 + checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" 49 + dependencies = [ 50 + "anstyle", 51 + "once_cell_polyfill", 52 + "windows-sys 0.61.2", 53 + ] 54 + 55 + [[package]] 56 + name = "anyhow" 57 + version = "1.0.102" 58 + source = "registry+https://github.com/rust-lang/crates.io-index" 59 + checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" 60 + 61 + [[package]] 62 + name = "atomic-waker" 63 + version = "1.1.2" 64 + source = "registry+https://github.com/rust-lang/crates.io-index" 65 + checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" 66 + 67 + [[package]] 68 + name = "base64" 69 + version = "0.22.1" 70 + source = "registry+https://github.com/rust-lang/crates.io-index" 71 + checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 72 + 73 + [[package]] 74 + name = "bitflags" 75 + version = "2.11.0" 76 + source = "registry+https://github.com/rust-lang/crates.io-index" 77 + checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" 78 + 79 + [[package]] 80 + name = "bumpalo" 81 + version = "3.20.2" 82 + source = "registry+https://github.com/rust-lang/crates.io-index" 83 + checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" 84 + 85 + [[package]] 86 + name = "bytes" 87 + version = "1.11.1" 88 + source = "registry+https://github.com/rust-lang/crates.io-index" 89 + checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" 90 + 91 + [[package]] 92 + name = "cc" 93 + version = "1.2.56" 94 + source = "registry+https://github.com/rust-lang/crates.io-index" 95 + checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" 96 + dependencies = [ 97 + "find-msvc-tools", 98 + "shlex", 99 + ] 100 + 101 + [[package]] 102 + name = "cfg-if" 103 + version = "1.0.4" 104 + source = "registry+https://github.com/rust-lang/crates.io-index" 105 + checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" 106 + 107 + [[package]] 108 + name = "cfg_aliases" 109 + version = "0.2.1" 110 + source = "registry+https://github.com/rust-lang/crates.io-index" 111 + checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 112 + 113 + [[package]] 114 + name = "clap" 115 + version = "4.5.60" 116 + source = "registry+https://github.com/rust-lang/crates.io-index" 117 + checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" 118 + dependencies = [ 119 + "clap_builder", 120 + "clap_derive", 121 + ] 122 + 123 + [[package]] 124 + name = "clap_builder" 125 + version = "4.5.60" 126 + source = "registry+https://github.com/rust-lang/crates.io-index" 127 + checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" 128 + dependencies = [ 129 + "anstream", 130 + "anstyle", 131 + "clap_lex", 132 + "strsim", 133 + ] 134 + 135 + [[package]] 136 + name = "clap_derive" 137 + version = "4.5.55" 138 + source = "registry+https://github.com/rust-lang/crates.io-index" 139 + checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" 140 + dependencies = [ 141 + "heck", 142 + "proc-macro2", 143 + "quote", 144 + "syn", 145 + ] 146 + 147 + [[package]] 148 + name = "clap_lex" 149 + version = "1.0.0" 150 + source = "registry+https://github.com/rust-lang/crates.io-index" 151 + checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" 152 + 153 + [[package]] 154 + name = "colorchoice" 155 + version = "1.0.4" 156 + source = "registry+https://github.com/rust-lang/crates.io-index" 157 + checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" 158 + 159 + [[package]] 160 + name = "displaydoc" 161 + version = "0.2.5" 162 + source = "registry+https://github.com/rust-lang/crates.io-index" 163 + checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 164 + dependencies = [ 165 + "proc-macro2", 166 + "quote", 167 + "syn", 168 + ] 169 + 170 + [[package]] 171 + name = "errno" 172 + version = "0.3.14" 173 + source = "registry+https://github.com/rust-lang/crates.io-index" 174 + checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" 175 + dependencies = [ 176 + "libc", 177 + "windows-sys 0.61.2", 178 + ] 179 + 180 + [[package]] 181 + name = "find-msvc-tools" 182 + version = "0.1.9" 183 + source = "registry+https://github.com/rust-lang/crates.io-index" 184 + checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" 185 + 186 + [[package]] 187 + name = "form_urlencoded" 188 + version = "1.2.2" 189 + source = "registry+https://github.com/rust-lang/crates.io-index" 190 + checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" 191 + dependencies = [ 192 + "percent-encoding", 193 + ] 194 + 195 + [[package]] 196 + name = "futures-channel" 197 + version = "0.3.32" 198 + source = "registry+https://github.com/rust-lang/crates.io-index" 199 + checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" 200 + dependencies = [ 201 + "futures-core", 202 + ] 203 + 204 + [[package]] 205 + name = "futures-core" 206 + version = "0.3.32" 207 + source = "registry+https://github.com/rust-lang/crates.io-index" 208 + checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" 209 + 210 + [[package]] 211 + name = "futures-task" 212 + version = "0.3.32" 213 + source = "registry+https://github.com/rust-lang/crates.io-index" 214 + checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" 215 + 216 + [[package]] 217 + name = "futures-util" 218 + version = "0.3.32" 219 + source = "registry+https://github.com/rust-lang/crates.io-index" 220 + checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" 221 + dependencies = [ 222 + "futures-core", 223 + "futures-task", 224 + "pin-project-lite", 225 + "slab", 226 + ] 227 + 228 + [[package]] 229 + name = "getrandom" 230 + version = "0.2.17" 231 + source = "registry+https://github.com/rust-lang/crates.io-index" 232 + checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" 233 + dependencies = [ 234 + "cfg-if", 235 + "js-sys", 236 + "libc", 237 + "wasi", 238 + "wasm-bindgen", 239 + ] 240 + 241 + [[package]] 242 + name = "getrandom" 243 + version = "0.3.4" 244 + source = "registry+https://github.com/rust-lang/crates.io-index" 245 + checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" 246 + dependencies = [ 247 + "cfg-if", 248 + "js-sys", 249 + "libc", 250 + "r-efi", 251 + "wasip2", 252 + "wasm-bindgen", 253 + ] 254 + 255 + [[package]] 256 + name = "heck" 257 + version = "0.5.0" 258 + source = "registry+https://github.com/rust-lang/crates.io-index" 259 + checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 260 + 261 + [[package]] 262 + name = "http" 263 + version = "1.4.0" 264 + source = "registry+https://github.com/rust-lang/crates.io-index" 265 + checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" 266 + dependencies = [ 267 + "bytes", 268 + "itoa", 269 + ] 270 + 271 + [[package]] 272 + name = "http-body" 273 + version = "1.0.1" 274 + source = "registry+https://github.com/rust-lang/crates.io-index" 275 + checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" 276 + dependencies = [ 277 + "bytes", 278 + "http", 279 + ] 280 + 281 + [[package]] 282 + name = "http-body-util" 283 + version = "0.1.3" 284 + source = "registry+https://github.com/rust-lang/crates.io-index" 285 + checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" 286 + dependencies = [ 287 + "bytes", 288 + "futures-core", 289 + "http", 290 + "http-body", 291 + "pin-project-lite", 292 + ] 293 + 294 + [[package]] 295 + name = "httparse" 296 + version = "1.10.1" 297 + source = "registry+https://github.com/rust-lang/crates.io-index" 298 + checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" 299 + 300 + [[package]] 301 + name = "hyper" 302 + version = "1.8.1" 303 + source = "registry+https://github.com/rust-lang/crates.io-index" 304 + checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" 305 + dependencies = [ 306 + "atomic-waker", 307 + "bytes", 308 + "futures-channel", 309 + "futures-core", 310 + "http", 311 + "http-body", 312 + "httparse", 313 + "itoa", 314 + "pin-project-lite", 315 + "pin-utils", 316 + "smallvec", 317 + "tokio", 318 + "want", 319 + ] 320 + 321 + [[package]] 322 + name = "hyper-rustls" 323 + version = "0.27.7" 324 + source = "registry+https://github.com/rust-lang/crates.io-index" 325 + checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" 326 + dependencies = [ 327 + "http", 328 + "hyper", 329 + "hyper-util", 330 + "rustls", 331 + "rustls-pki-types", 332 + "tokio", 333 + "tokio-rustls", 334 + "tower-service", 335 + "webpki-roots", 336 + ] 337 + 338 + [[package]] 339 + name = "hyper-util" 340 + version = "0.1.20" 341 + source = "registry+https://github.com/rust-lang/crates.io-index" 342 + checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" 343 + dependencies = [ 344 + "base64", 345 + "bytes", 346 + "futures-channel", 347 + "futures-util", 348 + "http", 349 + "http-body", 350 + "hyper", 351 + "ipnet", 352 + "libc", 353 + "percent-encoding", 354 + "pin-project-lite", 355 + "socket2", 356 + "tokio", 357 + "tower-service", 358 + "tracing", 359 + ] 360 + 361 + [[package]] 362 + name = "icu_collections" 363 + version = "2.1.1" 364 + source = "registry+https://github.com/rust-lang/crates.io-index" 365 + checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" 366 + dependencies = [ 367 + "displaydoc", 368 + "potential_utf", 369 + "yoke", 370 + "zerofrom", 371 + "zerovec", 372 + ] 373 + 374 + [[package]] 375 + name = "icu_locale_core" 376 + version = "2.1.1" 377 + source = "registry+https://github.com/rust-lang/crates.io-index" 378 + checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" 379 + dependencies = [ 380 + "displaydoc", 381 + "litemap", 382 + "tinystr", 383 + "writeable", 384 + "zerovec", 385 + ] 386 + 387 + [[package]] 388 + name = "icu_normalizer" 389 + version = "2.1.1" 390 + source = "registry+https://github.com/rust-lang/crates.io-index" 391 + checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" 392 + dependencies = [ 393 + "icu_collections", 394 + "icu_normalizer_data", 395 + "icu_properties", 396 + "icu_provider", 397 + "smallvec", 398 + "zerovec", 399 + ] 400 + 401 + [[package]] 402 + name = "icu_normalizer_data" 403 + version = "2.1.1" 404 + source = "registry+https://github.com/rust-lang/crates.io-index" 405 + checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" 406 + 407 + [[package]] 408 + name = "icu_properties" 409 + version = "2.1.2" 410 + source = "registry+https://github.com/rust-lang/crates.io-index" 411 + checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" 412 + dependencies = [ 413 + "icu_collections", 414 + "icu_locale_core", 415 + "icu_properties_data", 416 + "icu_provider", 417 + "zerotrie", 418 + "zerovec", 419 + ] 420 + 421 + [[package]] 422 + name = "icu_properties_data" 423 + version = "2.1.2" 424 + source = "registry+https://github.com/rust-lang/crates.io-index" 425 + checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" 426 + 427 + [[package]] 428 + name = "icu_provider" 429 + version = "2.1.1" 430 + source = "registry+https://github.com/rust-lang/crates.io-index" 431 + checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" 432 + dependencies = [ 433 + "displaydoc", 434 + "icu_locale_core", 435 + "writeable", 436 + "yoke", 437 + "zerofrom", 438 + "zerotrie", 439 + "zerovec", 440 + ] 441 + 442 + [[package]] 443 + name = "idna" 444 + version = "1.1.0" 445 + source = "registry+https://github.com/rust-lang/crates.io-index" 446 + checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" 447 + dependencies = [ 448 + "idna_adapter", 449 + "smallvec", 450 + "utf8_iter", 451 + ] 452 + 453 + [[package]] 454 + name = "idna_adapter" 455 + version = "1.2.1" 456 + source = "registry+https://github.com/rust-lang/crates.io-index" 457 + checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" 458 + dependencies = [ 459 + "icu_normalizer", 460 + "icu_properties", 461 + ] 462 + 463 + [[package]] 464 + name = "ipnet" 465 + version = "2.11.0" 466 + source = "registry+https://github.com/rust-lang/crates.io-index" 467 + checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" 468 + 469 + [[package]] 470 + name = "iri-string" 471 + version = "0.7.10" 472 + source = "registry+https://github.com/rust-lang/crates.io-index" 473 + checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" 474 + dependencies = [ 475 + "memchr", 476 + "serde", 477 + ] 478 + 479 + [[package]] 480 + name = "is_terminal_polyfill" 481 + version = "1.70.2" 482 + source = "registry+https://github.com/rust-lang/crates.io-index" 483 + checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" 484 + 485 + [[package]] 486 + name = "itoa" 487 + version = "1.0.17" 488 + source = "registry+https://github.com/rust-lang/crates.io-index" 489 + checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" 490 + 491 + [[package]] 492 + name = "js-sys" 493 + version = "0.3.91" 494 + source = "registry+https://github.com/rust-lang/crates.io-index" 495 + checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" 496 + dependencies = [ 497 + "once_cell", 498 + "wasm-bindgen", 499 + ] 500 + 501 + [[package]] 502 + name = "libc" 503 + version = "0.2.182" 504 + source = "registry+https://github.com/rust-lang/crates.io-index" 505 + checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" 506 + 507 + [[package]] 508 + name = "litemap" 509 + version = "0.8.1" 510 + source = "registry+https://github.com/rust-lang/crates.io-index" 511 + checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" 512 + 513 + [[package]] 514 + name = "lock_api" 515 + version = "0.4.14" 516 + source = "registry+https://github.com/rust-lang/crates.io-index" 517 + checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" 518 + dependencies = [ 519 + "scopeguard", 520 + ] 521 + 522 + [[package]] 523 + name = "log" 524 + version = "0.4.29" 525 + source = "registry+https://github.com/rust-lang/crates.io-index" 526 + checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" 527 + 528 + [[package]] 529 + name = "lru-slab" 530 + version = "0.1.2" 531 + source = "registry+https://github.com/rust-lang/crates.io-index" 532 + checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" 533 + 534 + [[package]] 535 + name = "memchr" 536 + version = "2.8.0" 537 + source = "registry+https://github.com/rust-lang/crates.io-index" 538 + checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" 539 + 540 + [[package]] 541 + name = "mio" 542 + version = "1.1.1" 543 + source = "registry+https://github.com/rust-lang/crates.io-index" 544 + checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" 545 + dependencies = [ 546 + "libc", 547 + "wasi", 548 + "windows-sys 0.61.2", 549 + ] 550 + 551 + [[package]] 552 + name = "once_cell" 553 + version = "1.21.3" 554 + source = "registry+https://github.com/rust-lang/crates.io-index" 555 + checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 556 + 557 + [[package]] 558 + name = "once_cell_polyfill" 559 + version = "1.70.2" 560 + source = "registry+https://github.com/rust-lang/crates.io-index" 561 + checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" 562 + 563 + [[package]] 564 + name = "opake-cli" 565 + version = "0.1.0" 566 + dependencies = [ 567 + "anyhow", 568 + "clap", 569 + "opake-core", 570 + "reqwest", 571 + "serde", 572 + "serde_json", 573 + "tokio", 574 + ] 575 + 576 + [[package]] 577 + name = "opake-core" 578 + version = "0.1.0" 579 + dependencies = [ 580 + "serde", 581 + "serde_json", 582 + "thiserror", 583 + ] 584 + 585 + [[package]] 586 + name = "parking_lot" 587 + version = "0.12.5" 588 + source = "registry+https://github.com/rust-lang/crates.io-index" 589 + checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" 590 + dependencies = [ 591 + "lock_api", 592 + "parking_lot_core", 593 + ] 594 + 595 + [[package]] 596 + name = "parking_lot_core" 597 + version = "0.9.12" 598 + source = "registry+https://github.com/rust-lang/crates.io-index" 599 + checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" 600 + dependencies = [ 601 + "cfg-if", 602 + "libc", 603 + "redox_syscall", 604 + "smallvec", 605 + "windows-link", 606 + ] 607 + 608 + [[package]] 609 + name = "percent-encoding" 610 + version = "2.3.2" 611 + source = "registry+https://github.com/rust-lang/crates.io-index" 612 + checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" 613 + 614 + [[package]] 615 + name = "pin-project-lite" 616 + version = "0.2.17" 617 + source = "registry+https://github.com/rust-lang/crates.io-index" 618 + checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" 619 + 620 + [[package]] 621 + name = "pin-utils" 622 + version = "0.1.0" 623 + source = "registry+https://github.com/rust-lang/crates.io-index" 624 + checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 625 + 626 + [[package]] 627 + name = "potential_utf" 628 + version = "0.1.4" 629 + source = "registry+https://github.com/rust-lang/crates.io-index" 630 + checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" 631 + dependencies = [ 632 + "zerovec", 633 + ] 634 + 635 + [[package]] 636 + name = "ppv-lite86" 637 + version = "0.2.21" 638 + source = "registry+https://github.com/rust-lang/crates.io-index" 639 + checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" 640 + dependencies = [ 641 + "zerocopy", 642 + ] 643 + 644 + [[package]] 645 + name = "proc-macro2" 646 + version = "1.0.106" 647 + source = "registry+https://github.com/rust-lang/crates.io-index" 648 + checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" 649 + dependencies = [ 650 + "unicode-ident", 651 + ] 652 + 653 + [[package]] 654 + name = "quinn" 655 + version = "0.11.9" 656 + source = "registry+https://github.com/rust-lang/crates.io-index" 657 + checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" 658 + dependencies = [ 659 + "bytes", 660 + "cfg_aliases", 661 + "pin-project-lite", 662 + "quinn-proto", 663 + "quinn-udp", 664 + "rustc-hash", 665 + "rustls", 666 + "socket2", 667 + "thiserror", 668 + "tokio", 669 + "tracing", 670 + "web-time", 671 + ] 672 + 673 + [[package]] 674 + name = "quinn-proto" 675 + version = "0.11.13" 676 + source = "registry+https://github.com/rust-lang/crates.io-index" 677 + checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" 678 + dependencies = [ 679 + "bytes", 680 + "getrandom 0.3.4", 681 + "lru-slab", 682 + "rand", 683 + "ring", 684 + "rustc-hash", 685 + "rustls", 686 + "rustls-pki-types", 687 + "slab", 688 + "thiserror", 689 + "tinyvec", 690 + "tracing", 691 + "web-time", 692 + ] 693 + 694 + [[package]] 695 + name = "quinn-udp" 696 + version = "0.5.14" 697 + source = "registry+https://github.com/rust-lang/crates.io-index" 698 + checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" 699 + dependencies = [ 700 + "cfg_aliases", 701 + "libc", 702 + "once_cell", 703 + "socket2", 704 + "tracing", 705 + "windows-sys 0.60.2", 706 + ] 707 + 708 + [[package]] 709 + name = "quote" 710 + version = "1.0.44" 711 + source = "registry+https://github.com/rust-lang/crates.io-index" 712 + checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" 713 + dependencies = [ 714 + "proc-macro2", 715 + ] 716 + 717 + [[package]] 718 + name = "r-efi" 719 + version = "5.3.0" 720 + source = "registry+https://github.com/rust-lang/crates.io-index" 721 + checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" 722 + 723 + [[package]] 724 + name = "rand" 725 + version = "0.9.2" 726 + source = "registry+https://github.com/rust-lang/crates.io-index" 727 + checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" 728 + dependencies = [ 729 + "rand_chacha", 730 + "rand_core", 731 + ] 732 + 733 + [[package]] 734 + name = "rand_chacha" 735 + version = "0.9.0" 736 + source = "registry+https://github.com/rust-lang/crates.io-index" 737 + checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" 738 + dependencies = [ 739 + "ppv-lite86", 740 + "rand_core", 741 + ] 742 + 743 + [[package]] 744 + name = "rand_core" 745 + version = "0.9.5" 746 + source = "registry+https://github.com/rust-lang/crates.io-index" 747 + checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" 748 + dependencies = [ 749 + "getrandom 0.3.4", 750 + ] 751 + 752 + [[package]] 753 + name = "redox_syscall" 754 + version = "0.5.18" 755 + source = "registry+https://github.com/rust-lang/crates.io-index" 756 + checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" 757 + dependencies = [ 758 + "bitflags", 759 + ] 760 + 761 + [[package]] 762 + name = "reqwest" 763 + version = "0.12.28" 764 + source = "registry+https://github.com/rust-lang/crates.io-index" 765 + checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" 766 + dependencies = [ 767 + "base64", 768 + "bytes", 769 + "futures-core", 770 + "http", 771 + "http-body", 772 + "http-body-util", 773 + "hyper", 774 + "hyper-rustls", 775 + "hyper-util", 776 + "js-sys", 777 + "log", 778 + "percent-encoding", 779 + "pin-project-lite", 780 + "quinn", 781 + "rustls", 782 + "rustls-pki-types", 783 + "serde", 784 + "serde_json", 785 + "serde_urlencoded", 786 + "sync_wrapper", 787 + "tokio", 788 + "tokio-rustls", 789 + "tower", 790 + "tower-http", 791 + "tower-service", 792 + "url", 793 + "wasm-bindgen", 794 + "wasm-bindgen-futures", 795 + "web-sys", 796 + "webpki-roots", 797 + ] 798 + 799 + [[package]] 800 + name = "ring" 801 + version = "0.17.14" 802 + source = "registry+https://github.com/rust-lang/crates.io-index" 803 + checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" 804 + dependencies = [ 805 + "cc", 806 + "cfg-if", 807 + "getrandom 0.2.17", 808 + "libc", 809 + "untrusted", 810 + "windows-sys 0.52.0", 811 + ] 812 + 813 + [[package]] 814 + name = "rustc-hash" 815 + version = "2.1.1" 816 + source = "registry+https://github.com/rust-lang/crates.io-index" 817 + checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" 818 + 819 + [[package]] 820 + name = "rustls" 821 + version = "0.23.37" 822 + source = "registry+https://github.com/rust-lang/crates.io-index" 823 + checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" 824 + dependencies = [ 825 + "once_cell", 826 + "ring", 827 + "rustls-pki-types", 828 + "rustls-webpki", 829 + "subtle", 830 + "zeroize", 831 + ] 832 + 833 + [[package]] 834 + name = "rustls-pki-types" 835 + version = "1.14.0" 836 + source = "registry+https://github.com/rust-lang/crates.io-index" 837 + checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" 838 + dependencies = [ 839 + "web-time", 840 + "zeroize", 841 + ] 842 + 843 + [[package]] 844 + name = "rustls-webpki" 845 + version = "0.103.9" 846 + source = "registry+https://github.com/rust-lang/crates.io-index" 847 + checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" 848 + dependencies = [ 849 + "ring", 850 + "rustls-pki-types", 851 + "untrusted", 852 + ] 853 + 854 + [[package]] 855 + name = "rustversion" 856 + version = "1.0.22" 857 + source = "registry+https://github.com/rust-lang/crates.io-index" 858 + checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" 859 + 860 + [[package]] 861 + name = "ryu" 862 + version = "1.0.23" 863 + source = "registry+https://github.com/rust-lang/crates.io-index" 864 + checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" 865 + 866 + [[package]] 867 + name = "scopeguard" 868 + version = "1.2.0" 869 + source = "registry+https://github.com/rust-lang/crates.io-index" 870 + checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 871 + 872 + [[package]] 873 + name = "serde" 874 + version = "1.0.228" 875 + source = "registry+https://github.com/rust-lang/crates.io-index" 876 + checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" 877 + dependencies = [ 878 + "serde_core", 879 + "serde_derive", 880 + ] 881 + 882 + [[package]] 883 + name = "serde_core" 884 + version = "1.0.228" 885 + source = "registry+https://github.com/rust-lang/crates.io-index" 886 + checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" 887 + dependencies = [ 888 + "serde_derive", 889 + ] 890 + 891 + [[package]] 892 + name = "serde_derive" 893 + version = "1.0.228" 894 + source = "registry+https://github.com/rust-lang/crates.io-index" 895 + checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" 896 + dependencies = [ 897 + "proc-macro2", 898 + "quote", 899 + "syn", 900 + ] 901 + 902 + [[package]] 903 + name = "serde_json" 904 + version = "1.0.149" 905 + source = "registry+https://github.com/rust-lang/crates.io-index" 906 + checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" 907 + dependencies = [ 908 + "itoa", 909 + "memchr", 910 + "serde", 911 + "serde_core", 912 + "zmij", 913 + ] 914 + 915 + [[package]] 916 + name = "serde_urlencoded" 917 + version = "0.7.1" 918 + source = "registry+https://github.com/rust-lang/crates.io-index" 919 + checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 920 + dependencies = [ 921 + "form_urlencoded", 922 + "itoa", 923 + "ryu", 924 + "serde", 925 + ] 926 + 927 + [[package]] 928 + name = "shlex" 929 + version = "1.3.0" 930 + source = "registry+https://github.com/rust-lang/crates.io-index" 931 + checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 932 + 933 + [[package]] 934 + name = "signal-hook-registry" 935 + version = "1.4.8" 936 + source = "registry+https://github.com/rust-lang/crates.io-index" 937 + checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" 938 + dependencies = [ 939 + "errno", 940 + "libc", 941 + ] 942 + 943 + [[package]] 944 + name = "slab" 945 + version = "0.4.12" 946 + source = "registry+https://github.com/rust-lang/crates.io-index" 947 + checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" 948 + 949 + [[package]] 950 + name = "smallvec" 951 + version = "1.15.1" 952 + source = "registry+https://github.com/rust-lang/crates.io-index" 953 + checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" 954 + 955 + [[package]] 956 + name = "socket2" 957 + version = "0.6.2" 958 + source = "registry+https://github.com/rust-lang/crates.io-index" 959 + checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" 960 + dependencies = [ 961 + "libc", 962 + "windows-sys 0.60.2", 963 + ] 964 + 965 + [[package]] 966 + name = "stable_deref_trait" 967 + version = "1.2.1" 968 + source = "registry+https://github.com/rust-lang/crates.io-index" 969 + checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" 970 + 971 + [[package]] 972 + name = "strsim" 973 + version = "0.11.1" 974 + source = "registry+https://github.com/rust-lang/crates.io-index" 975 + checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 976 + 977 + [[package]] 978 + name = "subtle" 979 + version = "2.6.1" 980 + source = "registry+https://github.com/rust-lang/crates.io-index" 981 + checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" 982 + 983 + [[package]] 984 + name = "syn" 985 + version = "2.0.117" 986 + source = "registry+https://github.com/rust-lang/crates.io-index" 987 + checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" 988 + dependencies = [ 989 + "proc-macro2", 990 + "quote", 991 + "unicode-ident", 992 + ] 993 + 994 + [[package]] 995 + name = "sync_wrapper" 996 + version = "1.0.2" 997 + source = "registry+https://github.com/rust-lang/crates.io-index" 998 + checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" 999 + dependencies = [ 1000 + "futures-core", 1001 + ] 1002 + 1003 + [[package]] 1004 + name = "synstructure" 1005 + version = "0.13.2" 1006 + source = "registry+https://github.com/rust-lang/crates.io-index" 1007 + checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" 1008 + dependencies = [ 1009 + "proc-macro2", 1010 + "quote", 1011 + "syn", 1012 + ] 1013 + 1014 + [[package]] 1015 + name = "thiserror" 1016 + version = "2.0.18" 1017 + source = "registry+https://github.com/rust-lang/crates.io-index" 1018 + checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" 1019 + dependencies = [ 1020 + "thiserror-impl", 1021 + ] 1022 + 1023 + [[package]] 1024 + name = "thiserror-impl" 1025 + version = "2.0.18" 1026 + source = "registry+https://github.com/rust-lang/crates.io-index" 1027 + checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" 1028 + dependencies = [ 1029 + "proc-macro2", 1030 + "quote", 1031 + "syn", 1032 + ] 1033 + 1034 + [[package]] 1035 + name = "tinystr" 1036 + version = "0.8.2" 1037 + source = "registry+https://github.com/rust-lang/crates.io-index" 1038 + checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" 1039 + dependencies = [ 1040 + "displaydoc", 1041 + "zerovec", 1042 + ] 1043 + 1044 + [[package]] 1045 + name = "tinyvec" 1046 + version = "1.10.0" 1047 + source = "registry+https://github.com/rust-lang/crates.io-index" 1048 + checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" 1049 + dependencies = [ 1050 + "tinyvec_macros", 1051 + ] 1052 + 1053 + [[package]] 1054 + name = "tinyvec_macros" 1055 + version = "0.1.1" 1056 + source = "registry+https://github.com/rust-lang/crates.io-index" 1057 + checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 1058 + 1059 + [[package]] 1060 + name = "tokio" 1061 + version = "1.49.0" 1062 + source = "registry+https://github.com/rust-lang/crates.io-index" 1063 + checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" 1064 + dependencies = [ 1065 + "bytes", 1066 + "libc", 1067 + "mio", 1068 + "parking_lot", 1069 + "pin-project-lite", 1070 + "signal-hook-registry", 1071 + "socket2", 1072 + "tokio-macros", 1073 + "windows-sys 0.61.2", 1074 + ] 1075 + 1076 + [[package]] 1077 + name = "tokio-macros" 1078 + version = "2.6.0" 1079 + source = "registry+https://github.com/rust-lang/crates.io-index" 1080 + checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" 1081 + dependencies = [ 1082 + "proc-macro2", 1083 + "quote", 1084 + "syn", 1085 + ] 1086 + 1087 + [[package]] 1088 + name = "tokio-rustls" 1089 + version = "0.26.4" 1090 + source = "registry+https://github.com/rust-lang/crates.io-index" 1091 + checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" 1092 + dependencies = [ 1093 + "rustls", 1094 + "tokio", 1095 + ] 1096 + 1097 + [[package]] 1098 + name = "tower" 1099 + version = "0.5.3" 1100 + source = "registry+https://github.com/rust-lang/crates.io-index" 1101 + checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" 1102 + dependencies = [ 1103 + "futures-core", 1104 + "futures-util", 1105 + "pin-project-lite", 1106 + "sync_wrapper", 1107 + "tokio", 1108 + "tower-layer", 1109 + "tower-service", 1110 + ] 1111 + 1112 + [[package]] 1113 + name = "tower-http" 1114 + version = "0.6.8" 1115 + source = "registry+https://github.com/rust-lang/crates.io-index" 1116 + checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" 1117 + dependencies = [ 1118 + "bitflags", 1119 + "bytes", 1120 + "futures-util", 1121 + "http", 1122 + "http-body", 1123 + "iri-string", 1124 + "pin-project-lite", 1125 + "tower", 1126 + "tower-layer", 1127 + "tower-service", 1128 + ] 1129 + 1130 + [[package]] 1131 + name = "tower-layer" 1132 + version = "0.3.3" 1133 + source = "registry+https://github.com/rust-lang/crates.io-index" 1134 + checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" 1135 + 1136 + [[package]] 1137 + name = "tower-service" 1138 + version = "0.3.3" 1139 + source = "registry+https://github.com/rust-lang/crates.io-index" 1140 + checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" 1141 + 1142 + [[package]] 1143 + name = "tracing" 1144 + version = "0.1.44" 1145 + source = "registry+https://github.com/rust-lang/crates.io-index" 1146 + checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" 1147 + dependencies = [ 1148 + "pin-project-lite", 1149 + "tracing-core", 1150 + ] 1151 + 1152 + [[package]] 1153 + name = "tracing-core" 1154 + version = "0.1.36" 1155 + source = "registry+https://github.com/rust-lang/crates.io-index" 1156 + checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" 1157 + dependencies = [ 1158 + "once_cell", 1159 + ] 1160 + 1161 + [[package]] 1162 + name = "try-lock" 1163 + version = "0.2.5" 1164 + source = "registry+https://github.com/rust-lang/crates.io-index" 1165 + checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 1166 + 1167 + [[package]] 1168 + name = "unicode-ident" 1169 + version = "1.0.24" 1170 + source = "registry+https://github.com/rust-lang/crates.io-index" 1171 + checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" 1172 + 1173 + [[package]] 1174 + name = "untrusted" 1175 + version = "0.9.0" 1176 + source = "registry+https://github.com/rust-lang/crates.io-index" 1177 + checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" 1178 + 1179 + [[package]] 1180 + name = "url" 1181 + version = "2.5.8" 1182 + source = "registry+https://github.com/rust-lang/crates.io-index" 1183 + checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" 1184 + dependencies = [ 1185 + "form_urlencoded", 1186 + "idna", 1187 + "percent-encoding", 1188 + "serde", 1189 + ] 1190 + 1191 + [[package]] 1192 + name = "utf8_iter" 1193 + version = "1.0.4" 1194 + source = "registry+https://github.com/rust-lang/crates.io-index" 1195 + checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 1196 + 1197 + [[package]] 1198 + name = "utf8parse" 1199 + version = "0.2.2" 1200 + source = "registry+https://github.com/rust-lang/crates.io-index" 1201 + checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 1202 + 1203 + [[package]] 1204 + name = "want" 1205 + version = "0.3.1" 1206 + source = "registry+https://github.com/rust-lang/crates.io-index" 1207 + checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" 1208 + dependencies = [ 1209 + "try-lock", 1210 + ] 1211 + 1212 + [[package]] 1213 + name = "wasi" 1214 + version = "0.11.1+wasi-snapshot-preview1" 1215 + source = "registry+https://github.com/rust-lang/crates.io-index" 1216 + checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" 1217 + 1218 + [[package]] 1219 + name = "wasip2" 1220 + version = "1.0.2+wasi-0.2.9" 1221 + source = "registry+https://github.com/rust-lang/crates.io-index" 1222 + checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" 1223 + dependencies = [ 1224 + "wit-bindgen", 1225 + ] 1226 + 1227 + [[package]] 1228 + name = "wasm-bindgen" 1229 + version = "0.2.114" 1230 + source = "registry+https://github.com/rust-lang/crates.io-index" 1231 + checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" 1232 + dependencies = [ 1233 + "cfg-if", 1234 + "once_cell", 1235 + "rustversion", 1236 + "wasm-bindgen-macro", 1237 + "wasm-bindgen-shared", 1238 + ] 1239 + 1240 + [[package]] 1241 + name = "wasm-bindgen-futures" 1242 + version = "0.4.64" 1243 + source = "registry+https://github.com/rust-lang/crates.io-index" 1244 + checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" 1245 + dependencies = [ 1246 + "cfg-if", 1247 + "futures-util", 1248 + "js-sys", 1249 + "once_cell", 1250 + "wasm-bindgen", 1251 + "web-sys", 1252 + ] 1253 + 1254 + [[package]] 1255 + name = "wasm-bindgen-macro" 1256 + version = "0.2.114" 1257 + source = "registry+https://github.com/rust-lang/crates.io-index" 1258 + checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" 1259 + dependencies = [ 1260 + "quote", 1261 + "wasm-bindgen-macro-support", 1262 + ] 1263 + 1264 + [[package]] 1265 + name = "wasm-bindgen-macro-support" 1266 + version = "0.2.114" 1267 + source = "registry+https://github.com/rust-lang/crates.io-index" 1268 + checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" 1269 + dependencies = [ 1270 + "bumpalo", 1271 + "proc-macro2", 1272 + "quote", 1273 + "syn", 1274 + "wasm-bindgen-shared", 1275 + ] 1276 + 1277 + [[package]] 1278 + name = "wasm-bindgen-shared" 1279 + version = "0.2.114" 1280 + source = "registry+https://github.com/rust-lang/crates.io-index" 1281 + checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" 1282 + dependencies = [ 1283 + "unicode-ident", 1284 + ] 1285 + 1286 + [[package]] 1287 + name = "web-sys" 1288 + version = "0.3.91" 1289 + source = "registry+https://github.com/rust-lang/crates.io-index" 1290 + checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" 1291 + dependencies = [ 1292 + "js-sys", 1293 + "wasm-bindgen", 1294 + ] 1295 + 1296 + [[package]] 1297 + name = "web-time" 1298 + version = "1.1.0" 1299 + source = "registry+https://github.com/rust-lang/crates.io-index" 1300 + checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" 1301 + dependencies = [ 1302 + "js-sys", 1303 + "wasm-bindgen", 1304 + ] 1305 + 1306 + [[package]] 1307 + name = "webpki-roots" 1308 + version = "1.0.6" 1309 + source = "registry+https://github.com/rust-lang/crates.io-index" 1310 + checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" 1311 + dependencies = [ 1312 + "rustls-pki-types", 1313 + ] 1314 + 1315 + [[package]] 1316 + name = "windows-link" 1317 + version = "0.2.1" 1318 + source = "registry+https://github.com/rust-lang/crates.io-index" 1319 + checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" 1320 + 1321 + [[package]] 1322 + name = "windows-sys" 1323 + version = "0.52.0" 1324 + source = "registry+https://github.com/rust-lang/crates.io-index" 1325 + checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1326 + dependencies = [ 1327 + "windows-targets 0.52.6", 1328 + ] 1329 + 1330 + [[package]] 1331 + name = "windows-sys" 1332 + version = "0.60.2" 1333 + source = "registry+https://github.com/rust-lang/crates.io-index" 1334 + checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" 1335 + dependencies = [ 1336 + "windows-targets 0.53.5", 1337 + ] 1338 + 1339 + [[package]] 1340 + name = "windows-sys" 1341 + version = "0.61.2" 1342 + source = "registry+https://github.com/rust-lang/crates.io-index" 1343 + checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" 1344 + dependencies = [ 1345 + "windows-link", 1346 + ] 1347 + 1348 + [[package]] 1349 + name = "windows-targets" 1350 + version = "0.52.6" 1351 + source = "registry+https://github.com/rust-lang/crates.io-index" 1352 + checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1353 + dependencies = [ 1354 + "windows_aarch64_gnullvm 0.52.6", 1355 + "windows_aarch64_msvc 0.52.6", 1356 + "windows_i686_gnu 0.52.6", 1357 + "windows_i686_gnullvm 0.52.6", 1358 + "windows_i686_msvc 0.52.6", 1359 + "windows_x86_64_gnu 0.52.6", 1360 + "windows_x86_64_gnullvm 0.52.6", 1361 + "windows_x86_64_msvc 0.52.6", 1362 + ] 1363 + 1364 + [[package]] 1365 + name = "windows-targets" 1366 + version = "0.53.5" 1367 + source = "registry+https://github.com/rust-lang/crates.io-index" 1368 + checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" 1369 + dependencies = [ 1370 + "windows-link", 1371 + "windows_aarch64_gnullvm 0.53.1", 1372 + "windows_aarch64_msvc 0.53.1", 1373 + "windows_i686_gnu 0.53.1", 1374 + "windows_i686_gnullvm 0.53.1", 1375 + "windows_i686_msvc 0.53.1", 1376 + "windows_x86_64_gnu 0.53.1", 1377 + "windows_x86_64_gnullvm 0.53.1", 1378 + "windows_x86_64_msvc 0.53.1", 1379 + ] 1380 + 1381 + [[package]] 1382 + name = "windows_aarch64_gnullvm" 1383 + version = "0.52.6" 1384 + source = "registry+https://github.com/rust-lang/crates.io-index" 1385 + checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1386 + 1387 + [[package]] 1388 + name = "windows_aarch64_gnullvm" 1389 + version = "0.53.1" 1390 + source = "registry+https://github.com/rust-lang/crates.io-index" 1391 + checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" 1392 + 1393 + [[package]] 1394 + name = "windows_aarch64_msvc" 1395 + version = "0.52.6" 1396 + source = "registry+https://github.com/rust-lang/crates.io-index" 1397 + checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1398 + 1399 + [[package]] 1400 + name = "windows_aarch64_msvc" 1401 + version = "0.53.1" 1402 + source = "registry+https://github.com/rust-lang/crates.io-index" 1403 + checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" 1404 + 1405 + [[package]] 1406 + name = "windows_i686_gnu" 1407 + version = "0.52.6" 1408 + source = "registry+https://github.com/rust-lang/crates.io-index" 1409 + checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1410 + 1411 + [[package]] 1412 + name = "windows_i686_gnu" 1413 + version = "0.53.1" 1414 + source = "registry+https://github.com/rust-lang/crates.io-index" 1415 + checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" 1416 + 1417 + [[package]] 1418 + name = "windows_i686_gnullvm" 1419 + version = "0.52.6" 1420 + source = "registry+https://github.com/rust-lang/crates.io-index" 1421 + checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1422 + 1423 + [[package]] 1424 + name = "windows_i686_gnullvm" 1425 + version = "0.53.1" 1426 + source = "registry+https://github.com/rust-lang/crates.io-index" 1427 + checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" 1428 + 1429 + [[package]] 1430 + name = "windows_i686_msvc" 1431 + version = "0.52.6" 1432 + source = "registry+https://github.com/rust-lang/crates.io-index" 1433 + checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1434 + 1435 + [[package]] 1436 + name = "windows_i686_msvc" 1437 + version = "0.53.1" 1438 + source = "registry+https://github.com/rust-lang/crates.io-index" 1439 + checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" 1440 + 1441 + [[package]] 1442 + name = "windows_x86_64_gnu" 1443 + version = "0.52.6" 1444 + source = "registry+https://github.com/rust-lang/crates.io-index" 1445 + checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1446 + 1447 + [[package]] 1448 + name = "windows_x86_64_gnu" 1449 + version = "0.53.1" 1450 + source = "registry+https://github.com/rust-lang/crates.io-index" 1451 + checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" 1452 + 1453 + [[package]] 1454 + name = "windows_x86_64_gnullvm" 1455 + version = "0.52.6" 1456 + source = "registry+https://github.com/rust-lang/crates.io-index" 1457 + checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1458 + 1459 + [[package]] 1460 + name = "windows_x86_64_gnullvm" 1461 + version = "0.53.1" 1462 + source = "registry+https://github.com/rust-lang/crates.io-index" 1463 + checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" 1464 + 1465 + [[package]] 1466 + name = "windows_x86_64_msvc" 1467 + version = "0.52.6" 1468 + source = "registry+https://github.com/rust-lang/crates.io-index" 1469 + checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1470 + 1471 + [[package]] 1472 + name = "windows_x86_64_msvc" 1473 + version = "0.53.1" 1474 + source = "registry+https://github.com/rust-lang/crates.io-index" 1475 + checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" 1476 + 1477 + [[package]] 1478 + name = "wit-bindgen" 1479 + version = "0.51.0" 1480 + source = "registry+https://github.com/rust-lang/crates.io-index" 1481 + checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" 1482 + 1483 + [[package]] 1484 + name = "writeable" 1485 + version = "0.6.2" 1486 + source = "registry+https://github.com/rust-lang/crates.io-index" 1487 + checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" 1488 + 1489 + [[package]] 1490 + name = "yoke" 1491 + version = "0.8.1" 1492 + source = "registry+https://github.com/rust-lang/crates.io-index" 1493 + checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" 1494 + dependencies = [ 1495 + "stable_deref_trait", 1496 + "yoke-derive", 1497 + "zerofrom", 1498 + ] 1499 + 1500 + [[package]] 1501 + name = "yoke-derive" 1502 + version = "0.8.1" 1503 + source = "registry+https://github.com/rust-lang/crates.io-index" 1504 + checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" 1505 + dependencies = [ 1506 + "proc-macro2", 1507 + "quote", 1508 + "syn", 1509 + "synstructure", 1510 + ] 1511 + 1512 + [[package]] 1513 + name = "zerocopy" 1514 + version = "0.8.40" 1515 + source = "registry+https://github.com/rust-lang/crates.io-index" 1516 + checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5" 1517 + dependencies = [ 1518 + "zerocopy-derive", 1519 + ] 1520 + 1521 + [[package]] 1522 + name = "zerocopy-derive" 1523 + version = "0.8.40" 1524 + source = "registry+https://github.com/rust-lang/crates.io-index" 1525 + checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" 1526 + dependencies = [ 1527 + "proc-macro2", 1528 + "quote", 1529 + "syn", 1530 + ] 1531 + 1532 + [[package]] 1533 + name = "zerofrom" 1534 + version = "0.1.6" 1535 + source = "registry+https://github.com/rust-lang/crates.io-index" 1536 + checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" 1537 + dependencies = [ 1538 + "zerofrom-derive", 1539 + ] 1540 + 1541 + [[package]] 1542 + name = "zerofrom-derive" 1543 + version = "0.1.6" 1544 + source = "registry+https://github.com/rust-lang/crates.io-index" 1545 + checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" 1546 + dependencies = [ 1547 + "proc-macro2", 1548 + "quote", 1549 + "syn", 1550 + "synstructure", 1551 + ] 1552 + 1553 + [[package]] 1554 + name = "zeroize" 1555 + version = "1.8.2" 1556 + source = "registry+https://github.com/rust-lang/crates.io-index" 1557 + checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" 1558 + 1559 + [[package]] 1560 + name = "zerotrie" 1561 + version = "0.2.3" 1562 + source = "registry+https://github.com/rust-lang/crates.io-index" 1563 + checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" 1564 + dependencies = [ 1565 + "displaydoc", 1566 + "yoke", 1567 + "zerofrom", 1568 + ] 1569 + 1570 + [[package]] 1571 + name = "zerovec" 1572 + version = "0.11.5" 1573 + source = "registry+https://github.com/rust-lang/crates.io-index" 1574 + checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" 1575 + dependencies = [ 1576 + "yoke", 1577 + "zerofrom", 1578 + "zerovec-derive", 1579 + ] 1580 + 1581 + [[package]] 1582 + name = "zerovec-derive" 1583 + version = "0.11.2" 1584 + source = "registry+https://github.com/rust-lang/crates.io-index" 1585 + checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" 1586 + dependencies = [ 1587 + "proc-macro2", 1588 + "quote", 1589 + "syn", 1590 + ] 1591 + 1592 + [[package]] 1593 + name = "zmij" 1594 + version = "1.0.21" 1595 + source = "registry+https://github.com/rust-lang/crates.io-index" 1596 + checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
+12
Cargo.toml
··· 1 + [workspace] 2 + members = ["crates/opake-core", "crates/opake-cli"] 3 + resolver = "2" 4 + 5 + [workspace.package] 6 + edition = "2021" 7 + version = "0.1.0" 8 + 9 + [workspace.dependencies] 10 + serde = { version = "1", features = ["derive"] } 11 + serde_json = "1" 12 + thiserror = "2"
+18
crates/opake-cli/Cargo.toml
··· 1 + [package] 2 + name = "opake-cli" 3 + description = "CLI for Opake encrypted personal cloud" 4 + edition.workspace = true 5 + version.workspace = true 6 + 7 + [[bin]] 8 + name = "opake" 9 + path = "src/main.rs" 10 + 11 + [dependencies] 12 + opake-core = { path = "../opake-core" } 13 + clap = { version = "4", features = ["derive"] } 14 + tokio = { version = "1", features = ["full"] } 15 + anyhow = "1" 16 + reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } 17 + serde.workspace = true 18 + serde_json.workspace = true
+35
crates/opake-cli/src/config.rs
··· 1 + use std::path::PathBuf; 2 + 3 + use opake_core::client::{Session, XrpcClient}; 4 + use serde::{Deserialize, Serialize}; 5 + 6 + use crate::transport::ReqwestTransport; 7 + 8 + /// Persistent CLI configuration (PDS URL, preferences). 9 + #[derive(Debug, Serialize, Deserialize)] 10 + pub struct Config { 11 + pub pds_url: String, 12 + } 13 + 14 + /// Where Opake stores its state on disk. 15 + fn data_dir() -> PathBuf { 16 + let home = std::env::var("HOME").expect("HOME not set"); 17 + PathBuf::from(home).join(".config").join("opake") 18 + } 19 + 20 + pub fn config_path() -> PathBuf { 21 + data_dir().join("config.toml") 22 + } 23 + 24 + pub fn session_path() -> PathBuf { 25 + data_dir().join("session.json") 26 + } 27 + 28 + /// Restore a saved session and build an authenticated XRPC client. 29 + pub fn load_client() -> anyhow::Result<XrpcClient<ReqwestTransport>> { 30 + todo!("read session from {}, reconstruct client", session_path().display()) 31 + } 32 + 33 + pub fn save_session(session: &Session) -> anyhow::Result<()> { 34 + todo!("write session to {}", session_path().display()) 35 + }
+105
crates/opake-cli/src/main.rs
··· 1 + mod config; 2 + mod transport; 3 + 4 + use clap::{Parser, Subcommand}; 5 + use opake_core::client::XrpcClient; 6 + use std::path::PathBuf; 7 + use transport::ReqwestTransport; 8 + 9 + #[derive(Parser)] 10 + #[command(name = "opake", about = "Encrypted personal cloud on AT Protocol")] 11 + struct Cli { 12 + #[command(subcommand)] 13 + command: Command, 14 + } 15 + 16 + #[derive(Subcommand)] 17 + enum Command { 18 + /// Authenticate with your PDS 19 + Login { 20 + /// PDS URL (e.g. https://pds.example.com) 21 + #[arg(long)] 22 + pds: String, 23 + 24 + /// Handle or DID 25 + #[arg(long)] 26 + identifier: String, 27 + 28 + /// App password 29 + #[arg(long)] 30 + password: String, 31 + }, 32 + 33 + /// Upload and encrypt a file 34 + Upload { 35 + /// Path to the file to encrypt and upload 36 + path: PathBuf, 37 + 38 + /// Encrypt under a keyring instead of direct keys 39 + #[arg(long)] 40 + keyring: Option<String>, 41 + 42 + /// Comma-separated tags for categorization 43 + #[arg(long, value_delimiter = ',')] 44 + tags: Vec<String>, 45 + }, 46 + 47 + /// Download and decrypt a file 48 + Download { 49 + /// AT URI of the document record 50 + uri: String, 51 + 52 + /// Output path (defaults to the original filename) 53 + #[arg(short, long)] 54 + output: Option<PathBuf>, 55 + }, 56 + 57 + /// List your documents 58 + Ls { 59 + /// Filter by tag 60 + #[arg(long)] 61 + tag: Option<String>, 62 + }, 63 + 64 + /// Delete a document 65 + Rm { 66 + /// AT URI of the document record 67 + uri: String, 68 + }, 69 + } 70 + 71 + #[tokio::main] 72 + async fn main() -> anyhow::Result<()> { 73 + let cli = Cli::parse(); 74 + 75 + match cli.command { 76 + Command::Login { pds, identifier, password } => { 77 + let transport = ReqwestTransport::new(); 78 + let mut client = XrpcClient::new(transport, pds); 79 + let session = client.login(&identifier, &password).await?; 80 + config::save_session(session)?; 81 + println!("logged in as {}", session.handle); 82 + Ok(()) 83 + } 84 + Command::Upload { path, keyring, tags } => { 85 + let _client = config::load_client()?; 86 + let _ = (path, keyring, tags); 87 + todo!("read file, generate content key, encrypt, upload blob, create document record") 88 + } 89 + Command::Download { uri, output } => { 90 + let _client = config::load_client()?; 91 + let _ = (uri, output); 92 + todo!("fetch document record, resolve encryption, fetch blob, decrypt, write to disk") 93 + } 94 + Command::Ls { tag } => { 95 + let _client = config::load_client()?; 96 + let _ = tag; 97 + todo!("list document records via com.atproto.repo.listRecords") 98 + } 99 + Command::Rm { uri } => { 100 + let _client = config::load_client()?; 101 + let _ = uri; 102 + todo!("delete document record via com.atproto.repo.deleteRecord") 103 + } 104 + } 105 + }
+49
crates/opake-cli/src/transport.rs
··· 1 + // reqwest-based Transport implementation for native (non-WASM) targets. 2 + 3 + use opake_core::client::{HttpMethod, HttpRequest, HttpResponse, RequestBody, Transport}; 4 + use opake_core::error::Error; 5 + 6 + pub struct ReqwestTransport { 7 + http: reqwest::Client, 8 + } 9 + 10 + impl ReqwestTransport { 11 + pub fn new() -> Self { 12 + Self { http: reqwest::Client::new() } 13 + } 14 + } 15 + 16 + impl Transport for ReqwestTransport { 17 + async fn send(&self, request: HttpRequest) -> Result<HttpResponse, Error> { 18 + let mut builder = match request.method { 19 + HttpMethod::Get => self.http.get(&request.url), 20 + HttpMethod::Post => self.http.post(&request.url), 21 + }; 22 + 23 + for (key, value) in &request.headers { 24 + builder = builder.header(key, value); 25 + } 26 + 27 + if let Some(body) = request.body { 28 + builder = match body { 29 + RequestBody::Json(json) => builder.json(&json), 30 + RequestBody::Bytes { data, content_type } => { 31 + builder.header("Content-Type", content_type).body(data) 32 + } 33 + }; 34 + } 35 + 36 + let response = builder.send().await.map_err(|e| Error::Xrpc { 37 + status: e.status().map(|s| s.as_u16()).unwrap_or(0), 38 + message: e.to_string(), 39 + })?; 40 + 41 + let status = response.status().as_u16(); 42 + let body = response.bytes().await.map_err(|e| Error::Xrpc { 43 + status, 44 + message: e.to_string(), 45 + })?; 46 + 47 + Ok(HttpResponse { status, body: body.to_vec() }) 48 + } 49 + }
+16
crates/opake-core/Cargo.toml
··· 1 + [package] 2 + name = "opake-core" 3 + description = "Encryption, records, and XRPC client for Opake — platform-agnostic, WASM-compatible" 4 + edition.workspace = true 5 + version.workspace = true 6 + 7 + [dependencies] 8 + serde.workspace = true 9 + serde_json.workspace = true 10 + thiserror.workspace = true 11 + 12 + # Add when implementing crypto (Phase 1): 13 + # aes-gcm = "0.10" # AES-256-GCM content encryption 14 + # x25519-dalek = "2" # ECDH key agreement for key wrapping — WASM-safe 15 + # rand = "0.8" # key generation 16 + # base64 = "0.22" # atproto bytes encoding
+272
crates/opake-core/src/client.rs
··· 1 + // XRPC client for talking to a PDS. 2 + // 3 + // Transport is injected via trait — the CLI provides a reqwest-based 4 + // implementation, the SPA (opake-web) provides one backed by browser fetch. 5 + // Core owns the XRPC protocol logic (endpoints, auth, response parsing) 6 + // but never touches the network directly. 7 + 8 + use crate::error::Error; 9 + use crate::records::BlobRef; 10 + use serde::{Deserialize, Serialize}; 11 + 12 + // --------------------------------------------------------------------------- 13 + // Transport trait — the injectable I/O boundary 14 + // --------------------------------------------------------------------------- 15 + 16 + #[derive(Debug, Clone)] 17 + pub enum HttpMethod { 18 + Get, 19 + Post, 20 + } 21 + 22 + #[derive(Debug, Clone)] 23 + pub enum RequestBody { 24 + Json(serde_json::Value), 25 + Bytes { data: Vec<u8>, content_type: String }, 26 + } 27 + 28 + #[derive(Debug, Clone)] 29 + pub struct HttpRequest { 30 + pub method: HttpMethod, 31 + pub url: String, 32 + pub headers: Vec<(String, String)>, 33 + pub body: Option<RequestBody>, 34 + } 35 + 36 + #[derive(Debug, Clone)] 37 + pub struct HttpResponse { 38 + pub status: u16, 39 + pub body: Vec<u8>, 40 + } 41 + 42 + /// The only thing a platform needs to provide: send an HTTP request, get bytes back. 43 + /// CLI implements this with reqwest, the SPA with browser fetch via web_sys. 44 + pub trait Transport { 45 + fn send(&self, request: HttpRequest) -> impl std::future::Future<Output = Result<HttpResponse, Error>>; 46 + } 47 + 48 + // --------------------------------------------------------------------------- 49 + // XRPC client — protocol logic, generic over transport 50 + // --------------------------------------------------------------------------- 51 + 52 + /// An authenticated session with a PDS. 53 + #[derive(Debug, Clone, Serialize, Deserialize)] 54 + #[serde(rename_all = "camelCase")] 55 + pub struct Session { 56 + pub did: String, 57 + pub handle: String, 58 + pub access_jwt: String, 59 + pub refresh_jwt: String, 60 + } 61 + 62 + /// Reference to a created record. 63 + #[derive(Debug, Clone, Serialize, Deserialize)] 64 + pub struct RecordRef { 65 + pub uri: String, 66 + pub cid: String, 67 + } 68 + 69 + /// A page of records from `listRecords`. 70 + #[derive(Debug, Clone, Serialize, Deserialize)] 71 + pub struct RecordPage { 72 + pub records: Vec<RecordEntry>, 73 + pub cursor: Option<String>, 74 + } 75 + 76 + #[derive(Debug, Clone, Serialize, Deserialize)] 77 + pub struct RecordEntry { 78 + pub uri: String, 79 + pub cid: String, 80 + pub value: serde_json::Value, 81 + } 82 + 83 + pub struct XrpcClient<T: Transport> { 84 + transport: T, 85 + base_url: String, 86 + session: Option<Session>, 87 + } 88 + 89 + impl<T: Transport> XrpcClient<T> { 90 + pub fn new(transport: T, base_url: String) -> Self { 91 + Self { transport, base_url, session: None } 92 + } 93 + 94 + pub fn with_session(transport: T, base_url: String, session: Session) -> Self { 95 + Self { transport, base_url, session: Some(session) } 96 + } 97 + 98 + pub fn session(&self) -> Option<&Session> { 99 + self.session.as_ref() 100 + } 101 + 102 + /// Authenticate via `com.atproto.server.createSession`. 103 + pub async fn login(&mut self, identifier: &str, password: &str) -> Result<&Session, Error> { 104 + let body = serde_json::json!({ 105 + "identifier": identifier, 106 + "password": password, 107 + }); 108 + 109 + let response = self.transport.send(HttpRequest { 110 + method: HttpMethod::Post, 111 + url: format!("{}/xrpc/com.atproto.server.createSession", self.base_url), 112 + headers: vec![ 113 + ("Content-Type".into(), "application/json".into()), 114 + ], 115 + body: Some(RequestBody::Json(body)), 116 + }).await?; 117 + 118 + if response.status != 200 { 119 + return Err(Error::Auth(format!("login failed (HTTP {})", response.status))); 120 + } 121 + 122 + let session: Session = serde_json::from_slice(&response.body)?; 123 + self.session = Some(session); 124 + Ok(self.session.as_ref().unwrap()) 125 + } 126 + 127 + fn auth_header(&self) -> Result<(String, String), Error> { 128 + let session = self.session.as_ref().ok_or_else(|| Error::Auth("not logged in".into()))?; 129 + Ok(("Authorization".into(), format!("Bearer {}", session.access_jwt))) 130 + } 131 + 132 + fn did(&self) -> Result<&str, Error> { 133 + self.session.as_ref() 134 + .map(|s| s.did.as_str()) 135 + .ok_or_else(|| Error::Auth("not logged in".into())) 136 + } 137 + 138 + /// Upload raw bytes as a blob via `com.atproto.repo.uploadBlob`. 139 + pub async fn upload_blob(&self, data: Vec<u8>, mime_type: &str) -> Result<BlobRef, Error> { 140 + let auth = self.auth_header()?; 141 + 142 + let response = self.transport.send(HttpRequest { 143 + method: HttpMethod::Post, 144 + url: format!("{}/xrpc/com.atproto.repo.uploadBlob", self.base_url), 145 + headers: vec![auth, ("Content-Type".into(), mime_type.into())], 146 + body: Some(RequestBody::Bytes { data, content_type: mime_type.into() }), 147 + }).await?; 148 + 149 + #[derive(Deserialize)] 150 + struct UploadResponse { blob: BlobRef } 151 + let parsed: UploadResponse = serde_json::from_slice(&response.body)?; 152 + Ok(parsed.blob) 153 + } 154 + 155 + /// Fetch a blob by DID + CID via `com.atproto.sync.getBlob`. 156 + pub async fn get_blob(&self, did: &str, cid: &str) -> Result<Vec<u8>, Error> { 157 + let auth = self.auth_header()?; 158 + let url = format!( 159 + "{}/xrpc/com.atproto.sync.getBlob?did={}&cid={}", 160 + self.base_url, did, cid, 161 + ); 162 + 163 + let response = self.transport.send(HttpRequest { 164 + method: HttpMethod::Get, 165 + url, 166 + headers: vec![auth], 167 + body: None, 168 + }).await?; 169 + 170 + Ok(response.body) 171 + } 172 + 173 + /// Create a record via `com.atproto.repo.createRecord`. 174 + pub async fn create_record<R: Serialize>( 175 + &self, 176 + collection: &str, 177 + record: &R, 178 + ) -> Result<RecordRef, Error> { 179 + let auth = self.auth_header()?; 180 + let did = self.did()?; 181 + 182 + let body = serde_json::json!({ 183 + "repo": did, 184 + "collection": collection, 185 + "record": record, 186 + }); 187 + 188 + let response = self.transport.send(HttpRequest { 189 + method: HttpMethod::Post, 190 + url: format!("{}/xrpc/com.atproto.repo.createRecord", self.base_url), 191 + headers: vec![auth, ("Content-Type".into(), "application/json".into())], 192 + body: Some(RequestBody::Json(body)), 193 + }).await?; 194 + 195 + Ok(serde_json::from_slice(&response.body)?) 196 + } 197 + 198 + /// Fetch a single record via `com.atproto.repo.getRecord`. 199 + pub async fn get_record( 200 + &self, 201 + did: &str, 202 + collection: &str, 203 + rkey: &str, 204 + ) -> Result<RecordEntry, Error> { 205 + let auth = self.auth_header()?; 206 + let url = format!( 207 + "{}/xrpc/com.atproto.repo.getRecord?repo={}&collection={}&rkey={}", 208 + self.base_url, did, collection, rkey, 209 + ); 210 + 211 + let response = self.transport.send(HttpRequest { 212 + method: HttpMethod::Get, 213 + url, 214 + headers: vec![auth], 215 + body: None, 216 + }).await?; 217 + 218 + Ok(serde_json::from_slice(&response.body)?) 219 + } 220 + 221 + /// List records in a collection via `com.atproto.repo.listRecords`. 222 + pub async fn list_records( 223 + &self, 224 + collection: &str, 225 + limit: Option<u32>, 226 + cursor: Option<&str>, 227 + ) -> Result<RecordPage, Error> { 228 + let auth = self.auth_header()?; 229 + let did = self.did()?; 230 + 231 + let mut url = format!( 232 + "{}/xrpc/com.atproto.repo.listRecords?repo={}&collection={}", 233 + self.base_url, did, collection, 234 + ); 235 + if let Some(limit) = limit { 236 + url.push_str(&format!("&limit={}", limit)); 237 + } 238 + if let Some(cursor) = cursor { 239 + url.push_str(&format!("&cursor={}", cursor)); 240 + } 241 + 242 + let response = self.transport.send(HttpRequest { 243 + method: HttpMethod::Get, 244 + url, 245 + headers: vec![auth], 246 + body: None, 247 + }).await?; 248 + 249 + Ok(serde_json::from_slice(&response.body)?) 250 + } 251 + 252 + /// Delete a record via `com.atproto.repo.deleteRecord`. 253 + pub async fn delete_record(&self, collection: &str, rkey: &str) -> Result<(), Error> { 254 + let auth = self.auth_header()?; 255 + let did = self.did()?; 256 + 257 + let body = serde_json::json!({ 258 + "repo": did, 259 + "collection": collection, 260 + "rkey": rkey, 261 + }); 262 + 263 + self.transport.send(HttpRequest { 264 + method: HttpMethod::Post, 265 + url: format!("{}/xrpc/com.atproto.repo.deleteRecord", self.base_url), 266 + headers: vec![auth, ("Content-Type".into(), "application/json".into())], 267 + body: Some(RequestBody::Json(body)), 268 + }).await?; 269 + 270 + Ok(()) 271 + } 272 + }
+70
crates/opake-core/src/crypto.rs
··· 1 + // Client-side encryption primitives. 2 + // 3 + // This module handles AES-256-GCM content encryption and asymmetric key 4 + // wrapping. It intentionally has no I/O — it takes bytes in and returns bytes 5 + // out. The calling layer (CLI or WASM) handles reading/writing files and 6 + // talking to the PDS. 7 + 8 + use crate::error::Error; 9 + use crate::records::WrappedKey; 10 + 11 + /// A 256-bit AES content encryption key. 12 + pub struct ContentKey(pub [u8; 32]); 13 + 14 + /// The result of encrypting plaintext content. 15 + pub struct EncryptedPayload { 16 + pub ciphertext: Vec<u8>, 17 + pub nonce: [u8; 12], 18 + } 19 + 20 + /// Generate a random AES-256-GCM content key. 21 + pub fn generate_content_key() -> ContentKey { 22 + todo!() 23 + } 24 + 25 + /// Encrypt plaintext bytes with a content key (AES-256-GCM). 26 + pub fn encrypt_blob(key: &ContentKey, plaintext: &[u8]) -> Result<EncryptedPayload, Error> { 27 + todo!() 28 + } 29 + 30 + /// Decrypt an encrypted payload with a content key. 31 + pub fn decrypt_blob(key: &ContentKey, payload: &EncryptedPayload) -> Result<Vec<u8>, Error> { 32 + todo!() 33 + } 34 + 35 + /// Wrap a content key to a recipient's public key (ECDH-ES+A256KW). 36 + pub fn wrap_key( 37 + content_key: &ContentKey, 38 + recipient_public_key: &[u8], 39 + recipient_did: &str, 40 + ) -> Result<WrappedKey, Error> { 41 + todo!() 42 + } 43 + 44 + /// Unwrap a content key using the local private key. 45 + pub fn unwrap_key(wrapped: &WrappedKey, private_key: &[u8]) -> Result<ContentKey, Error> { 46 + todo!() 47 + } 48 + 49 + /// Generate a random group key for a keyring, then wrap it to a set of DIDs. 50 + pub fn create_group_key( 51 + member_public_keys: &[(&str, &[u8])], // (did, pubkey) pairs 52 + ) -> Result<(ContentKey, Vec<WrappedKey>), Error> { 53 + todo!() 54 + } 55 + 56 + /// Wrap a per-document content key under a keyring's group key (symmetric wrapping). 57 + pub fn wrap_content_key_for_keyring( 58 + content_key: &ContentKey, 59 + group_key: &ContentKey, 60 + ) -> Result<Vec<u8>, Error> { 61 + todo!() 62 + } 63 + 64 + /// Unwrap a per-document content key using the keyring's group key. 65 + pub fn unwrap_content_key_from_keyring( 66 + wrapped: &[u8], 67 + group_key: &ContentKey, 68 + ) -> Result<ContentKey, Error> { 69 + todo!() 70 + }
+28
crates/opake-core/src/error.rs
··· 1 + use thiserror::Error; 2 + 3 + #[derive(Debug, Error)] 4 + pub enum Error { 5 + #[error("encryption failed: {0}")] 6 + Encryption(String), 7 + 8 + #[error("decryption failed: {0}")] 9 + Decryption(String), 10 + 11 + #[error("key wrapping failed: {0}")] 12 + KeyWrap(String), 13 + 14 + #[error("authentication failed: {0}")] 15 + Auth(String), 16 + 17 + #[error("XRPC error ({status}): {message}")] 18 + Xrpc { status: u16, message: String }, 19 + 20 + #[error("record not found: {0}")] 21 + NotFound(String), 22 + 23 + #[error("invalid record: {0}")] 24 + InvalidRecord(String), 25 + 26 + #[error("{0}")] 27 + Serialization(#[from] serde_json::Error), 28 + }
+14
crates/opake-core/src/lib.rs
··· 1 + // opake-core: encryption, record types, and XRPC client. 2 + // 3 + // This crate is the shared foundation for both the CLI (opake-cli) and the 4 + // browser client (opake-web, via wasm-pack). Nothing in here depends on a 5 + // specific async runtime, filesystem, or platform API. 6 + // 7 + // Network I/O is injected via the `client::Transport` trait — the CLI provides 8 + // a reqwest-based implementation, the SPA provides one using browser fetch. 9 + // Crypto is synchronous and pure. Records are just types. 10 + 11 + pub mod client; 12 + pub mod crypto; 13 + pub mod error; 14 + pub mod records;
+160
crates/opake-core/src/records.rs
··· 1 + // Typed representations of the app.opake.cloud.* lexicon records. 2 + // 3 + // These mirror the lexicon JSON schemas and handle atproto's serialization 4 + // conventions ($type discriminators, $bytes for binary data, $link for CIDs). 5 + 6 + use serde::{Deserialize, Serialize}; 7 + 8 + // --------------------------------------------------------------------------- 9 + // AT Protocol primitives 10 + // --------------------------------------------------------------------------- 11 + 12 + /// Binary data in atproto JSON: `{ "$bytes": "<base64>" }`. 13 + #[derive(Debug, Clone, Serialize, Deserialize)] 14 + pub struct AtBytes { 15 + #[serde(rename = "$bytes")] 16 + pub encoded: String, 17 + } 18 + 19 + /// CID link reference: `{ "$link": "<cid>" }`. 20 + #[derive(Debug, Clone, Serialize, Deserialize)] 21 + pub struct CidLink { 22 + #[serde(rename = "$link")] 23 + pub cid: String, 24 + } 25 + 26 + /// Blob reference as returned by `com.atproto.repo.uploadBlob`. 27 + #[derive(Debug, Clone, Serialize, Deserialize)] 28 + #[serde(rename_all = "camelCase")] 29 + pub struct BlobRef { 30 + #[serde(rename = "$type")] 31 + pub blob_type: String, 32 + #[serde(rename = "ref")] 33 + pub reference: CidLink, 34 + pub mime_type: String, 35 + pub size: u64, 36 + } 37 + 38 + // --------------------------------------------------------------------------- 39 + // app.opake.cloud.defs 40 + // --------------------------------------------------------------------------- 41 + 42 + /// A symmetric key encrypted (wrapped) to a specific DID's public key. 43 + #[derive(Debug, Clone, Serialize, Deserialize)] 44 + pub struct WrappedKey { 45 + pub did: String, 46 + pub ciphertext: AtBytes, 47 + pub algo: String, 48 + } 49 + 50 + /// Describes how a blob's content was symmetrically encrypted, plus one or 51 + /// more wrapped copies of the content key for authorized DIDs. 52 + #[derive(Debug, Clone, Serialize, Deserialize)] 53 + pub struct EncryptionEnvelope { 54 + pub algo: String, 55 + pub nonce: AtBytes, 56 + pub keys: Vec<WrappedKey>, 57 + } 58 + 59 + /// Reference to a keyring whose group key protects the content key. 60 + #[derive(Debug, Clone, Serialize, Deserialize)] 61 + #[serde(rename_all = "camelCase")] 62 + pub struct KeyringRef { 63 + pub keyring: String, 64 + pub wrapped_content_key: AtBytes, 65 + pub rotation: u64, 66 + } 67 + 68 + // --------------------------------------------------------------------------- 69 + // app.opake.cloud.document — encryption union 70 + // --------------------------------------------------------------------------- 71 + 72 + /// Content key wrapped directly to individual DIDs. 73 + #[derive(Debug, Clone, Serialize, Deserialize)] 74 + pub struct DirectEncryption { 75 + pub envelope: EncryptionEnvelope, 76 + } 77 + 78 + /// Content key wrapped under a keyring's group key. 79 + #[derive(Debug, Clone, Serialize, Deserialize)] 80 + #[serde(rename_all = "camelCase")] 81 + pub struct KeyringEncryption { 82 + pub keyring_ref: KeyringRef, 83 + pub algo: String, 84 + pub nonce: AtBytes, 85 + } 86 + 87 + /// How to decrypt the blob — discriminated by `$type`. 88 + #[derive(Debug, Clone, Serialize, Deserialize)] 89 + #[serde(tag = "$type")] 90 + pub enum Encryption { 91 + #[serde(rename = "app.opake.cloud.document#directEncryption")] 92 + Direct(DirectEncryption), 93 + #[serde(rename = "app.opake.cloud.document#keyringEncryption")] 94 + Keyring(KeyringEncryption), 95 + } 96 + 97 + // --------------------------------------------------------------------------- 98 + // app.opake.cloud.document 99 + // --------------------------------------------------------------------------- 100 + 101 + #[derive(Debug, Clone, Serialize, Deserialize)] 102 + #[serde(rename_all = "camelCase")] 103 + pub struct Document { 104 + pub name: String, 105 + #[serde(skip_serializing_if = "Option::is_none")] 106 + pub mime_type: Option<String>, 107 + #[serde(skip_serializing_if = "Option::is_none")] 108 + pub size: Option<u64>, 109 + pub blob: BlobRef, 110 + pub encryption: Encryption, 111 + #[serde(default, skip_serializing_if = "Vec::is_empty")] 112 + pub tags: Vec<String>, 113 + #[serde(skip_serializing_if = "Option::is_none")] 114 + pub parent: Option<String>, 115 + #[serde(skip_serializing_if = "Option::is_none")] 116 + pub description: Option<String>, 117 + #[serde(skip_serializing_if = "Option::is_none")] 118 + pub visibility: Option<String>, 119 + pub created_at: String, 120 + #[serde(skip_serializing_if = "Option::is_none")] 121 + pub modified_at: Option<String>, 122 + } 123 + 124 + // --------------------------------------------------------------------------- 125 + // app.opake.cloud.grant 126 + // --------------------------------------------------------------------------- 127 + 128 + #[derive(Debug, Clone, Serialize, Deserialize)] 129 + #[serde(rename_all = "camelCase")] 130 + pub struct Grant { 131 + pub document: String, 132 + pub recipient: String, 133 + pub wrapped_key: WrappedKey, 134 + #[serde(skip_serializing_if = "Option::is_none")] 135 + pub permissions: Option<String>, 136 + #[serde(skip_serializing_if = "Option::is_none")] 137 + pub expires_at: Option<String>, 138 + #[serde(skip_serializing_if = "Option::is_none")] 139 + pub note: Option<String>, 140 + pub created_at: String, 141 + } 142 + 143 + // --------------------------------------------------------------------------- 144 + // app.opake.cloud.keyring 145 + // --------------------------------------------------------------------------- 146 + 147 + #[derive(Debug, Clone, Serialize, Deserialize)] 148 + #[serde(rename_all = "camelCase")] 149 + pub struct Keyring { 150 + pub name: String, 151 + #[serde(skip_serializing_if = "Option::is_none")] 152 + pub description: Option<String>, 153 + pub algo: String, 154 + pub members: Vec<WrappedKey>, 155 + #[serde(default)] 156 + pub rotation: u64, 157 + pub created_at: String, 158 + #[serde(skip_serializing_if = "Option::is_none")] 159 + pub modified_at: Option<String>, 160 + }
+178
lexicons/EXAMPLES.md
··· 1 + # Example Records 2 + 3 + ## 1. Alice creates a private encrypted document 4 + 5 + ```json 6 + { 7 + "$type": "app.opake.cloud.document", 8 + "name": "tax-return-2025.pdf", 9 + "mimeType": "application/pdf", 10 + "size": 284619, 11 + "blob": { 12 + "$type": "blob", 13 + "ref": { "$link": "bafkrei..." }, 14 + "mimeType": "application/octet-stream", 15 + "size": 284640 16 + }, 17 + "encryption": { 18 + "$type": "app.opake.cloud.document#directEncryption", 19 + "envelope": { 20 + "algo": "aes-256-gcm", 21 + "nonce": { "$bytes": "base64-encoded-12-byte-nonce" }, 22 + "keys": [ 23 + { 24 + "did": "did:plc:alice123", 25 + "ciphertext": { "$bytes": "base64-wrapped-content-key-for-alice" }, 26 + "algo": "ECDH-ES+A256KW" 27 + } 28 + ] 29 + } 30 + }, 31 + "tags": ["tax", "finance", "2025"], 32 + "visibility": "private", 33 + "createdAt": "2026-02-27T10:30:00.000Z" 34 + } 35 + ``` 36 + 37 + **What the PDS sees:** a record with some plaintext metadata (name, tags, timestamps) 38 + and an opaque blob. The `keys` array only contains Alice's wrapped key — only she 39 + can decrypt. 40 + 41 + 42 + ## 2. Alice shares the document with Bob via a grant 43 + 44 + ```json 45 + { 46 + "$type": "app.opake.cloud.grant", 47 + "document": "at://did:plc:alice123/app.opake.cloud.document/3k...", 48 + "recipient": "did:plc:bob456", 49 + "wrappedKey": { 50 + "did": "did:plc:bob456", 51 + "ciphertext": { "$bytes": "base64-wrapped-content-key-for-bob" }, 52 + "algo": "ECDH-ES+A256KW" 53 + }, 54 + "permissions": "read", 55 + "note": "Here's the tax doc you asked about", 56 + "createdAt": "2026-02-27T11:00:00.000Z" 57 + } 58 + ``` 59 + 60 + **How Bob decrypts:** 61 + 1. His client/AppView discovers this grant (firehose, query, or notification) 62 + 2. Fetches the document record via the `document` AT URI 63 + 3. Uses his private key to decrypt `wrappedKey.ciphertext` → gets AES-256 content key 64 + 4. Fetches the blob via `com.atproto.sync.getBlob` 65 + 5. Decrypts the blob using the content key + nonce from the document's encryption envelope 66 + 67 + **To revoke:** Alice deletes the grant record. Bob's copy of the wrapped key is gone 68 + from the network (eventually). For true forward secrecy, Alice would also re-encrypt 69 + the document with a fresh content key. 70 + 71 + 72 + ## 3. Keyring-based group sharing (family photos) 73 + 74 + ### First, the keyring: 75 + 76 + ```json 77 + { 78 + "$type": "app.opake.cloud.keyring", 79 + "name": "family-photos", 80 + "description": "Shared photo collection for the family", 81 + "algo": "aes-256-gcm", 82 + "members": [ 83 + { 84 + "did": "did:plc:alice123", 85 + "ciphertext": { "$bytes": "base64-group-key-wrapped-for-alice" }, 86 + "algo": "ECDH-ES+A256KW" 87 + }, 88 + { 89 + "did": "did:plc:bob456", 90 + "ciphertext": { "$bytes": "base64-group-key-wrapped-for-bob" }, 91 + "algo": "ECDH-ES+A256KW" 92 + }, 93 + { 94 + "did": "did:plc:carol789", 95 + "ciphertext": { "$bytes": "base64-group-key-wrapped-for-carol" }, 96 + "algo": "ECDH-ES+A256KW" 97 + } 98 + ], 99 + "rotation": 0, 100 + "createdAt": "2026-01-15T09:00:00.000Z" 101 + } 102 + ``` 103 + 104 + ### Then, a document using the keyring: 105 + 106 + ```json 107 + { 108 + "$type": "app.opake.cloud.document", 109 + "name": "beach-sunset.jpg", 110 + "mimeType": "image/jpeg", 111 + "size": 3841029, 112 + "blob": { 113 + "$type": "blob", 114 + "ref": { "$link": "bafkrei..." }, 115 + "mimeType": "application/octet-stream", 116 + "size": 3841056 117 + }, 118 + "encryption": { 119 + "$type": "app.opake.cloud.document#keyringEncryption", 120 + "keyringRef": { 121 + "keyring": "at://did:plc:alice123/app.opake.cloud.keyring/3k...", 122 + "wrappedContentKey": { "$bytes": "base64-content-key-encrypted-with-group-key" }, 123 + "rotation": 0 124 + }, 125 + "algo": "aes-256-gcm", 126 + "nonce": { "$bytes": "base64-encoded-12-byte-nonce" } 127 + }, 128 + "tags": ["family", "vacation", "beach"], 129 + "visibility": "shared", 130 + "createdAt": "2026-02-20T16:45:00.000Z" 131 + } 132 + ``` 133 + 134 + **How any family member decrypts:** 135 + 1. Fetch the keyring record from the `keyring` AT URI 136 + 2. Find their own entry in `members`, decrypt with their private key → get group key GK 137 + 3. Decrypt `wrappedContentKey` with GK → get the per-document content key 138 + 4. Fetch + decrypt the blob with content key + nonce 139 + 140 + **Adding a new family member (did:plc:dave):** 141 + - Wrap GK to Dave's pubkey 142 + - Update the keyring record to add Dave to `members` 143 + - Dave can now decrypt *all* documents under this keyring. No per-document changes needed. 144 + 145 + **Removing a member:** 146 + - Increment `rotation`, generate new GK, re-wrap to remaining members 147 + - New documents use the new GK 148 + - Old documents remain readable with old GK (same limitation as git-crypt) 149 + - For true revocation of old content: re-encrypt affected documents with new content keys 150 + 151 + 152 + ## Design Decisions & Notes 153 + 154 + ### Why plaintext metadata? 155 + The `name`, `tags`, `mimeType`, and `description` fields are intentionally unencrypted. 156 + This allows your personal AppView to index and search your files server-side without 157 + needing access to the content encryption keys. It's a conscious tradeoff: someone 158 + inspecting your repo can see *that* you have a file called "tax-return-2025.pdf" tagged 159 + with "finance", but they can't read the actual PDF. 160 + 161 + If you want fully opaque storage, you can encrypt the name/tags too and handle 162 + search purely client-side. The schema supports this — just put garbage/generic 163 + strings in the plaintext fields and store the real metadata inside the encrypted blob. 164 + 165 + ### Why separate grant records? 166 + Instead of adding recipients directly to the document record (like adding to the 167 + `keys` array), grants are separate records because: 168 + - The document owner might not want to update the document record every time they share 169 + - Grants can be deleted independently (for revocation) 170 + - An AppView can efficiently query "what's shared with me?" across all documents 171 + - It matches the atproto pattern of small, independent records 172 + 173 + ### Why the two-layer key for keyrings? 174 + Documents under a keyring still have their own per-document content key, just 175 + wrapped under the group key instead of individual pubkeys. This means: 176 + - Rotating the group key doesn't require re-encrypting every document's content 177 + - Individual documents can be selectively re-encrypted without touching the keyring 178 + - The content key acts as a per-document nonce for the group key
+60
lexicons/README.md
··· 1 + # app.opake.cloud.* Lexicon Schemas 2 + 3 + An encrypted personal cloud built on AT Protocol. 4 + 5 + ## Architecture 6 + 7 + The encryption model follows the same hybrid pattern as git-crypt: 8 + - Each file/record is encrypted with a **random symmetric key** (AES-256-GCM) 9 + - That symmetric key is **wrapped** (encrypted) to each authorized DID's public key 10 + - Wrapped keys are stored as atproto records, publicly visible but useless without the private key 11 + - File content is uploaded as a PDS blob (opaque encrypted bytes) 12 + 13 + ## Lexicon Overview 14 + 15 + | NSID | Type | Purpose | 16 + |------|------|---------| 17 + | `app.opake.cloud.defs` | defs | Shared type definitions (encryption envelope, wrapped key, etc.) | 18 + | `app.opake.cloud.document` | record | An encrypted file/document with metadata | 19 + | `app.opake.cloud.keyring` | record | A named group with a shared symmetric key, wrapped to each member | 20 + | `app.opake.cloud.grant` | record | A share grant — gives a DID access to a specific document's key | 21 + 22 + ## Flow: Sharing a file with another DID 23 + 24 + ``` 25 + 1. Alice creates a document: 26 + - Generates random AES-256-GCM key K 27 + - Encrypts file content with K → uploads as blob 28 + - Wraps K to her own DID pubkey → stores in document record 29 + 30 + 2. Alice shares with Bob (did:plc:bob): 31 + - Resolves did:plc:bob → gets public key from DID document 32 + - Wraps K to Bob's pubkey 33 + - Creates a grant record pointing to the document, containing Bob's wrapped key 34 + 35 + 3. Bob's client: 36 + - Discovers grant record (via AppView query, or notification) 37 + - Fetches the referenced document record 38 + - Finds his wrapped key in the grant 39 + - Decrypts K with his private key 40 + - Fetches the encrypted blob via com.atproto.sync.getBlob 41 + - Decrypts blob with K 42 + ``` 43 + 44 + ## Flow: Group sharing via keyring 45 + 46 + ``` 47 + 1. Alice creates a keyring "family-photos": 48 + - Generates group symmetric key GK 49 + - Wraps GK to each member's DID pubkey 50 + - Stores as keyring record 51 + 52 + 2. Alice creates documents referencing the keyring: 53 + - Each document's content key is encrypted with GK (not individual pubkeys) 54 + - Any keyring member can derive K from GK 55 + 56 + 3. Adding a new member: 57 + - Alice wraps GK to the new member's pubkey 58 + - Updates the keyring record 59 + - New member can now decrypt all documents in the group — no per-document re-encryption needed 60 + ```
+89
lexicons/app.opake.cloud.defs.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "app.opake.cloud.defs", 4 + "defs": { 5 + "wrappedKey": { 6 + "type": "object", 7 + "description": "A symmetric key encrypted (wrapped) to a specific DID's public key. The recipient can decrypt this with their private key to recover the underlying symmetric key.", 8 + "required": ["did", "ciphertext", "algo"], 9 + "properties": { 10 + "did": { 11 + "type": "string", 12 + "format": "did", 13 + "description": "The DID whose public key was used to wrap this key." 14 + }, 15 + "ciphertext": { 16 + "type": "bytes", 17 + "description": "The symmetric key, encrypted to the DID's public key. Base64-encoded in JSON.", 18 + "maxLength": 512 19 + }, 20 + "algo": { 21 + "type": "string", 22 + "description": "Asymmetric algorithm used for key wrapping.", 23 + "knownValues": [ 24 + "ECDH-ES+A256KW", 25 + "x25519-xsalsa20-poly1305" 26 + ] 27 + } 28 + } 29 + }, 30 + 31 + "encryptionEnvelope": { 32 + "type": "object", 33 + "description": "Describes how a blob's content was encrypted. Contains the algorithm, nonce/IV, and one or more wrapped copies of the content encryption key.", 34 + "required": ["algo", "nonce", "keys"], 35 + "properties": { 36 + "algo": { 37 + "type": "string", 38 + "description": "Symmetric encryption algorithm used on the blob content.", 39 + "knownValues": ["aes-256-gcm"] 40 + }, 41 + "nonce": { 42 + "type": "bytes", 43 + "description": "The nonce / initialization vector used for encryption.", 44 + "maxLength": 24 45 + }, 46 + "keys": { 47 + "type": "array", 48 + "description": "One or more wrapped copies of the content encryption key, each encrypted to a different DID.", 49 + "items": { "type": "ref", "ref": "#wrappedKey" }, 50 + "minLength": 1, 51 + "maxLength": 100 52 + } 53 + } 54 + }, 55 + 56 + "keyringRef": { 57 + "type": "object", 58 + "description": "Instead of per-document key wrapping, references a keyring whose group key was used to encrypt the content key. The content key is stored encrypted under the group key.", 59 + "required": ["keyring", "wrappedContentKey", "rotation"], 60 + "properties": { 61 + "keyring": { 62 + "type": "string", 63 + "format": "at-uri", 64 + "description": "AT URI of the app.opake.cloud.keyring record." 65 + }, 66 + "wrappedContentKey": { 67 + "type": "bytes", 68 + "description": "The content encryption key, encrypted with the keyring's group symmetric key (using the same algo as the content encryption).", 69 + "maxLength": 256 70 + }, 71 + "rotation": { 72 + "type": "integer", 73 + "description": "The keyring rotation counter at the time this content key was wrapped. Clients use this to identify which generation of the group key to use for decryption.", 74 + "minimum": 0 75 + } 76 + } 77 + }, 78 + 79 + "visibility": { 80 + "type": "string", 81 + "description": "Hint for AppViews and clients about intended visibility of the content.", 82 + "knownValues": [ 83 + "private", 84 + "shared", 85 + "public" 86 + ] 87 + } 88 + } 89 + }
+108
lexicons/app.opake.cloud.document.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "app.opake.cloud.document", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "description": "An encrypted file or document stored in the personal cloud. The actual content is an encrypted blob; this record holds the metadata, encryption envelope, and optional plaintext metadata for discoverability.", 8 + "key": "tid", 9 + "record": { 10 + "type": "object", 11 + "required": ["name", "blob", "encryption", "createdAt"], 12 + "properties": { 13 + "name": { 14 + "type": "string", 15 + "description": "Human-readable filename or title. Plaintext — intentionally unencrypted for indexing/search by your own AppView.", 16 + "maxLength": 512 17 + }, 18 + "mimeType": { 19 + "type": "string", 20 + "description": "Original MIME type of the unencrypted content (e.g. 'application/pdf', 'image/png'). Plaintext metadata.", 21 + "maxLength": 128 22 + }, 23 + "size": { 24 + "type": "integer", 25 + "description": "Size of the original unencrypted content in bytes.", 26 + "minimum": 0 27 + }, 28 + "blob": { 29 + "type": "blob", 30 + "description": "The encrypted file content, uploaded via com.atproto.repo.uploadBlob. The PDS stores this as opaque bytes. MimeType on the blob itself will be 'application/octet-stream'.", 31 + "accept": ["*/*"], 32 + "maxSize": 52428800 33 + }, 34 + "encryption": { 35 + "type": "union", 36 + "description": "How to decrypt the blob. Either a full encryption envelope (with per-document wrapped keys) or a keyring reference (for group-based access).", 37 + "refs": [ 38 + "#directEncryption", 39 + "#keyringEncryption" 40 + ] 41 + }, 42 + "tags": { 43 + "type": "array", 44 + "description": "Optional plaintext tags for categorization and search. Keep these non-sensitive — they're public.", 45 + "items": { "type": "string", "maxLength": 128 }, 46 + "maxLength": 32 47 + }, 48 + "parent": { 49 + "type": "string", 50 + "format": "at-uri", 51 + "description": "Optional reference to a parent document (for folder-like hierarchy). Points to another app.opake.cloud.document record." 52 + }, 53 + "description": { 54 + "type": "string", 55 + "description": "Optional plaintext description or summary.", 56 + "maxLength": 1024 57 + }, 58 + "visibility": { 59 + "type": "ref", 60 + "ref": "app.opake.cloud.defs#visibility" 61 + }, 62 + "createdAt": { 63 + "type": "string", 64 + "format": "datetime" 65 + }, 66 + "modifiedAt": { 67 + "type": "string", 68 + "format": "datetime" 69 + } 70 + } 71 + } 72 + }, 73 + 74 + "directEncryption": { 75 + "type": "object", 76 + "description": "Encryption where the content key is wrapped directly to one or more DIDs. Used for private files or ad-hoc sharing.", 77 + "required": ["envelope"], 78 + "properties": { 79 + "envelope": { 80 + "type": "ref", 81 + "ref": "app.opake.cloud.defs#encryptionEnvelope" 82 + } 83 + } 84 + }, 85 + 86 + "keyringEncryption": { 87 + "type": "object", 88 + "description": "Encryption where the content key is wrapped under a keyring's group key. Anyone with access to the keyring can decrypt.", 89 + "required": ["keyringRef", "algo", "nonce"], 90 + "properties": { 91 + "keyringRef": { 92 + "type": "ref", 93 + "ref": "app.opake.cloud.defs#keyringRef" 94 + }, 95 + "algo": { 96 + "type": "string", 97 + "description": "Symmetric algorithm used for content encryption.", 98 + "knownValues": ["aes-256-gcm"] 99 + }, 100 + "nonce": { 101 + "type": "bytes", 102 + "description": "Nonce/IV for the content encryption.", 103 + "maxLength": 24 104 + } 105 + } 106 + } 107 + } 108 + }
+54
lexicons/app.opake.cloud.grant.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "app.opake.cloud.grant", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "description": "A share grant — gives a specific DID access to a specific document's content encryption key. Created by the document owner when sharing a file ad-hoc (outside of a keyring). The recipient discovers this via their AppView or a notification mechanism. To revoke: delete this record and optionally re-encrypt the document with a new key.", 8 + "key": "tid", 9 + "record": { 10 + "type": "object", 11 + "required": ["document", "recipient", "wrappedKey", "createdAt"], 12 + "properties": { 13 + "document": { 14 + "type": "string", 15 + "format": "at-uri", 16 + "description": "AT URI of the app.opake.cloud.document record being shared." 17 + }, 18 + "recipient": { 19 + "type": "string", 20 + "format": "did", 21 + "description": "The DID being granted access." 22 + }, 23 + "wrappedKey": { 24 + "type": "ref", 25 + "ref": "app.opake.cloud.defs#wrappedKey", 26 + "description": "The document's content encryption key, wrapped to the recipient's DID public key." 27 + }, 28 + "permissions": { 29 + "type": "string", 30 + "description": "What the recipient is allowed to do. Note: this is advisory — actual enforcement depends on the AppView / client. Cryptographically, anyone with the key can read.", 31 + "knownValues": [ 32 + "read", 33 + "read-write" 34 + ] 35 + }, 36 + "expiresAt": { 37 + "type": "string", 38 + "format": "datetime", 39 + "description": "Optional expiration. After this time, clients should stop serving the content (advisory — the wrapped key remains valid unless the document is re-encrypted)." 40 + }, 41 + "note": { 42 + "type": "string", 43 + "description": "Optional message to the recipient (plaintext).", 44 + "maxLength": 512 45 + }, 46 + "createdAt": { 47 + "type": "string", 48 + "format": "datetime" 49 + } 50 + } 51 + } 52 + } 53 + } 54 + }
+52
lexicons/app.opake.cloud.keyring.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "app.opake.cloud.keyring", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "description": "A named keyring for group-based access control. Contains a group symmetric key wrapped to each member's DID public key. Analogous to git-crypt's named keys — documents encrypted under this keyring are accessible to all members without per-document key management.", 8 + "key": "tid", 9 + "record": { 10 + "type": "object", 11 + "required": ["name", "algo", "members", "createdAt"], 12 + "properties": { 13 + "name": { 14 + "type": "string", 15 + "description": "Human-readable name for this keyring (e.g. 'family-photos', 'work-projects').", 16 + "maxLength": 256 17 + }, 18 + "description": { 19 + "type": "string", 20 + "description": "Optional description of this keyring's purpose.", 21 + "maxLength": 1024 22 + }, 23 + "algo": { 24 + "type": "string", 25 + "description": "The symmetric algorithm the group key is intended for.", 26 + "knownValues": ["aes-256-gcm"] 27 + }, 28 + "members": { 29 + "type": "array", 30 + "description": "The group key wrapped to each member's DID public key. Each entry allows that DID to recover the group key.", 31 + "items": { "type": "ref", "ref": "app.opake.cloud.defs#wrappedKey" }, 32 + "minLength": 1, 33 + "maxLength": 256 34 + }, 35 + "rotation": { 36 + "type": "integer", 37 + "description": "Key rotation counter. Increment when the group key is rotated (e.g. after revoking a member). Documents should reference the rotation they were encrypted under.", 38 + "minimum": 0 39 + }, 40 + "createdAt": { 41 + "type": "string", 42 + "format": "datetime" 43 + }, 44 + "modifiedAt": { 45 + "type": "string", 46 + "format": "datetime" 47 + } 48 + } 49 + } 50 + } 51 + } 52 + }