An encrypted personal cloud built on the AT Protocol.

Update blackbox tests and README for directory commands

Add blackbox test section 3 (Directories) covering mkdir, tree, cat,
mv, upload --dir, and path-based operations. Expand section 4 (Delete)
with path-based deletion, recursive delete, and empty directory
deletion. Renumber all subsequent sections.

Update README usage examples with mkdir, tree, cat, mv, upload --dir,
and path-based rm. Mark folder hierarchy as complete in roadmap. Update
--as flag documentation to include new commands.

Closes #159

sans-self.org 0c246aab 9f0f6913

Waiting for spindle ...
+195 -48
+173 -44
AGENT-BLACKBOX-TEST.md
··· 152 152 153 153 --- 154 154 155 - ## 3. Delete 155 + ## 3. Directories 156 + 157 + ### 3.1 Create directories 158 + 159 + ```bash 160 + A$ opake mkdir Photos 161 + # prints: Photos → at://did:plc:A/app.opake.cloud.directory/<rkey> 162 + 163 + A$ opake mkdir Archive 164 + ``` 165 + 166 + ### 3.2 Upload into a directory 167 + 168 + ```bash 169 + echo "beach photo" > /tmp/beach.jpg 170 + A$ opake upload /tmp/beach.jpg --dir Photos 171 + # prints: beach.jpg → at://... (in Photos) 172 + ``` 173 + 174 + ### 3.3 View the tree 175 + 176 + ```bash 177 + A$ opake tree 178 + # / 179 + # ├── Archive/ 180 + # ├── Photos/ 181 + # │ └── beach.jpg 182 + # └── test-direct.txt (if still present from section 2) 183 + ``` 184 + 185 + **Verify:** 186 + - Directories appear with trailing `/` 187 + - Documents appear as leaves 188 + - Indentation and box-drawing characters are correct 189 + 190 + ### 3.4 Cat a file (decrypt to stdout) 191 + 192 + ```bash 193 + A$ opake cat beach.jpg 194 + # prints "beach photo" to stdout (no file created) 195 + 196 + # path-based cat: 197 + A$ opake cat Photos/beach.jpg 198 + # same output 199 + ``` 200 + 201 + **Verify:** 202 + - Output is the decrypted plaintext 203 + - No prompt, no "saved to" message — just raw content 204 + 205 + ### 3.5 Move a file into a directory 206 + 207 + ```bash 208 + echo "meeting notes" > /tmp/notes.txt 209 + A$ opake upload /tmp/notes.txt 210 + A$ opake mv notes.txt Archive/ 211 + # prints: moved "notes.txt" → Archive/ 212 + 213 + A$ opake tree 214 + # Archive/ now contains notes.txt 215 + ``` 216 + 217 + ### 3.6 Rename a file 218 + 219 + ```bash 220 + A$ opake mv notes.txt meeting-notes.txt 221 + # prints: renamed "notes.txt" → "meeting-notes.txt" 222 + ``` 223 + 224 + ### 3.7 Move via path 225 + 226 + ```bash 227 + A$ opake mv Archive/meeting-notes.txt Photos/ 228 + # prints: moved "meeting-notes.txt" → Photos/ 229 + ``` 230 + 231 + ### 3.8 Rename a directory 156 232 157 233 ```bash 158 - A$ opake rm test-direct.txt 159 - # prompts "delete at://...? [y/N]" 234 + A$ opake mv Archive Old 235 + # prints: renamed "Archive" → "Old" 236 + ``` 237 + 238 + ### 3.9 Move into self is rejected 239 + 240 + ```bash 241 + A$ opake mv Photos Photos/ 242 + # should error: "cannot move a directory into itself" 243 + ``` 244 + 245 + --- 246 + 247 + ## 4. Delete 248 + 249 + ```bash 250 + A$ opake rm beach.jpg 251 + # prompts "delete beach.jpg? [y/N]" 160 252 # type y 161 253 162 254 A$ opake ls 163 255 # file is gone 164 256 ``` 165 257 166 - ### 3.1 Delete with --yes 258 + ### 4.1 Delete with --yes 167 259 168 260 ```bash 169 261 A$ opake upload /tmp/test-direct.txt ··· 171 263 # no prompt, immediate delete 172 264 ``` 173 265 266 + ### 4.2 Delete by path 267 + 268 + ```bash 269 + echo "delete me" > /tmp/deleteme.txt 270 + A$ opake upload /tmp/deleteme.txt --dir Photos 271 + A$ opake rm Photos/deleteme.txt -y 272 + # deletes the document and removes it from Photos' entry list 273 + ``` 274 + 275 + ### 4.3 Delete an empty directory 276 + 277 + ```bash 278 + A$ opake rm Old -y 279 + # deletes the directory (must be empty) 280 + ``` 281 + 282 + ### 4.4 Delete non-empty directory without -r fails 283 + 284 + ```bash 285 + A$ opake rm Photos 286 + # should error: "directory is not empty (N documents, M subdirectories) — use -r to delete recursively" 287 + ``` 288 + 289 + ### 4.5 Recursive delete 290 + 291 + ```bash 292 + echo "sunset" > /tmp/sunset.jpg 293 + A$ opake upload /tmp/sunset.jpg --dir Photos 294 + A$ opake rm -r Photos 295 + # prompts: "delete Photos/? (1 documents, 0 subdirectories) [y/N]" 296 + # type y 297 + # prints: deleted at://... (1 documents, 1 directories) 298 + 299 + A$ opake tree 300 + # Photos is gone 301 + ``` 302 + 174 303 --- 175 304 176 - ## 4. Sharing (Grants) 305 + ## 5. Sharing (Grants) 177 306 178 - ### 4.1 Upload a file to share 307 + ### 5.1 Upload a file to share 179 308 180 309 ```bash 181 310 echo "shared secret" > /tmp/shared-file.txt ··· 184 313 185 314 Save URI as `$SHARED_URI`. 186 315 187 - ### 4.2 Share with B 316 + ### 5.2 Share with B 188 317 189 318 ```bash 190 319 A$ opake share shared-file.txt <B-handle> --note "for your eyes only" ··· 193 322 194 323 Save grant URI as `$GRANT_URI`. 195 324 196 - ### 4.3 List outgoing grants 325 + ### 5.3 List outgoing grants 197 326 198 327 ```bash 199 328 A$ opake shared ··· 207 336 - Grant appears with B's DID as recipient 208 337 - Note shows up in long format 209 338 210 - ### 4.4 Download via grant (cross-PDS) 339 + ### 5.4 Download via grant (cross-PDS) 211 340 212 341 ```bash 213 342 B$ opake download --grant $GRANT_URI -o /tmp/shared-download.txt ··· 221 350 - Works even though the file lives on A's PDS 222 351 - B never needs to be a "member" of anything 223 352 224 - ### 4.5 Revoke 353 + ### 5.5 Revoke 225 354 226 355 ```bash 227 356 A$ opake revoke $GRANT_URI ··· 232 361 # grant is gone 233 362 ``` 234 363 235 - ### 4.6 Download after revoke fails 364 + ### 5.6 Download after revoke fails 236 365 237 366 ```bash 238 367 B$ opake download --grant $GRANT_URI -o /tmp/should-fail.txt ··· 241 370 242 371 --- 243 372 244 - ## 5. Keyrings 373 + ## 6. Keyrings 245 374 246 - ### 5.1 Create a keyring 375 + ### 6.1 Create a keyring 247 376 248 377 ```bash 249 378 A$ opake keyring create family-photos 250 379 # prints: family-photos → at://did:plc:A/app.opake.cloud.keyring/<kr-rkey> 251 380 ``` 252 381 253 - ### 5.2 List keyrings 382 + ### 6.2 List keyrings 254 383 255 384 ```bash 256 385 A$ opake keyring ls ··· 260 389 # includes URI and rotation count (0) 261 390 ``` 262 391 263 - ### 5.3 Upload under keyring 392 + ### 6.3 Upload under keyring 264 393 265 394 ```bash 266 395 echo "family photo metadata" > /tmp/photo.txt ··· 269 398 270 399 Save URI as `$KR_DOC_URI`. 271 400 272 - ### 5.4 Download own keyring-encrypted file 401 + ### 6.4 Download own keyring-encrypted file 273 402 274 403 ```bash 275 404 A$ opake download photo.txt -o /tmp/photo-download.txt ··· 277 406 # identical 278 407 ``` 279 408 280 - ### 5.5 Add member 409 + ### 6.5 Add member 281 410 282 411 ```bash 283 412 A$ opake keyring add-member family-photos <B-handle> ··· 287 416 # family-photos 2 member(s) 288 417 ``` 289 418 290 - ### 5.6 Member download (cross-PDS, new feature) 419 + ### 6.6 Member download (cross-PDS) 291 420 292 421 ```bash 293 422 B$ opake download --keyring-member $KR_DOC_URI -o /tmp/kr-member-download.txt ··· 301 430 - B fetched from A's PDS (unauthenticated) 302 431 - Group key is now cached locally for B 303 432 304 - ### 5.7 Subsequent downloads use cached key 433 + ### 6.7 Subsequent downloads use cached key 305 434 306 435 Upload a second file under the same keyring as A: 307 436 ··· 319 448 diff /tmp/photo2.txt /tmp/photo2-download.txt 320 449 ``` 321 450 322 - ### 5.8 Non-member is rejected 451 + ### 6.8 Non-member is rejected 323 452 324 453 ```bash 325 454 C$ opake download --keyring-member $KR_DOC_URI -o /tmp/should-fail.txt ··· 328 457 329 458 If C is not available, skip this test. 330 459 331 - ### 5.9 Remove member 460 + ### 6.9 Remove member 332 461 333 462 ```bash 334 463 A$ opake keyring remove-member family-photos <B-handle> ··· 342 471 # rotation count is now 1 343 472 ``` 344 473 345 - ### 5.10 Removed member cannot download new uploads 474 + ### 6.10 Removed member cannot download new uploads 346 475 347 476 After removal, upload a new file under the rotated keyring: 348 477 ··· 361 490 362 491 --- 363 492 364 - ## 6. Error Cases 493 + ## 7. Error Cases 365 494 366 - ### 6.1 Download nonexistent file 495 + ### 7.1 Download nonexistent file 367 496 368 497 ```bash 369 498 A$ opake download nonexistent-file.txt 370 499 # should error: not found 371 500 ``` 372 501 373 - ### 6.2 Invalid AT-URI 502 + ### 7.2 Invalid AT-URI 374 503 375 504 ```bash 376 505 A$ opake download "not-a-uri" 377 506 # should error: AT-URI parse failure 378 507 ``` 379 508 380 - ### 6.3 Wrong account downloads direct file 509 + ### 7.3 Wrong account downloads direct file 381 510 382 511 ```bash 383 512 B$ opake download $DOC_URI -o /tmp/wrong-account.txt ··· 386 515 387 516 (Only works if A still has a direct-encrypted file uploaded. Re-upload one if needed.) 388 517 389 - ### 6.4 --grant and --keyring-member conflict 518 + ### 7.4 --grant and --keyring-member conflict 390 519 391 520 ```bash 392 521 B$ opake download --grant at://x --keyring-member at://y 393 522 # should error: clap conflict (cannot use both flags) 394 523 ``` 395 524 396 - ### 6.5 --keyring-member on a direct-encrypted document 525 + ### 7.5 --keyring-member on a direct-encrypted document 397 526 398 527 ```bash 399 528 B$ opake download --keyring-member $SHARED_URI -o /tmp/should-fail.txt ··· 404 533 405 534 --- 406 535 407 - ## 7. AppView 536 + ## 8. AppView 408 537 409 538 The AppView is a separate binary (`opake-appview`) that indexes grants and keyrings from the AT Protocol firehose. These tests require a running Jetstream instance or network access to the public Jetstream relays. 410 539 411 - ### 7.1 Configuration 540 + ### 8.1 Configuration 412 541 413 542 Create a minimal config: 414 543 ··· 420 549 EOF 421 550 ``` 422 551 423 - ### 7.2 Status (cold start) 552 + ### 8.2 Status (cold start) 424 553 425 554 ```bash 426 555 opake-appview --config-dir /tmp/opake-appview-test status ··· 429 558 # Keyrings: 0 430 559 ``` 431 560 432 - ### 7.3 Start indexer + API 561 + ### 8.3 Start indexer + API 433 562 434 563 ```bash 435 564 opake-appview --config-dir /tmp/opake-appview-test run -v & ··· 441 570 - Logs show "opake-appview listening on 127.0.0.1:6100" 442 571 - Logs show Jetstream connection established 443 572 444 - ### 7.4 Health endpoint 573 + ### 8.4 Health endpoint 445 574 446 575 ```bash 447 576 curl -s http://127.0.0.1:6100/api/health | jq . ··· 456 585 - `indexerConnected` is `true` 457 586 - `cursorAgeSecs` is small (< 60) 458 587 459 - ### 7.5 Inbox and keyrings require auth 588 + ### 8.5 Inbox and keyrings require auth 460 589 461 590 ```bash 462 591 curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:6100/api/inbox?did=did:plc:test ··· 466 595 # 401 467 596 ``` 468 597 469 - ### 7.6 Share triggers indexing 598 + ### 8.6 Share triggers indexing 470 599 471 600 With the AppView still running, create a share grant using the CLI (from section 4.2): 472 601 ··· 481 610 # Grants: should be ≥ 1 482 611 ``` 483 612 484 - ### 7.7 Status (after indexing) 613 + ### 8.7 Status (after indexing) 485 614 486 615 ```bash 487 616 opake-appview --config-dir /tmp/opake-appview-test status ··· 491 620 # Keyrings: <number> 492 621 ``` 493 622 494 - ### 7.8 Config dir matches CLI 623 + ### 8.8 Config dir matches CLI 495 624 496 625 Both binaries should resolve the same config directory: 497 626 ··· 504 633 opake-appview --config-dir /tmp/opake-appview-test status 505 634 ``` 506 635 507 - ### 7.9 Cleanup 636 + ### 8.9 Cleanup 508 637 509 638 ```bash 510 639 kill $APPVIEW_PID 2>/dev/null ··· 513 642 514 643 --- 515 644 516 - ## 8. Cleanup 645 + ## 9. Cleanup 517 646 518 647 ```bash 519 - # remove test files 520 - A$ opake rm photo.txt -y 521 - A$ opake rm photo2.txt -y 522 - A$ opake rm empty.bin -y 648 + # remove test files (some may already be deleted from section 4) 649 + A$ opake rm photo.txt -y 2>/dev/null 650 + A$ opake rm photo2.txt -y 2>/dev/null 651 + A$ opake rm empty.bin -y 2>/dev/null 523 652 # etc. 524 653 525 - rm /tmp/test-direct*.txt /tmp/shared-*.txt /tmp/photo*.txt /tmp/empty* /tmp/kr-* /tmp/should-fail.txt 2>/dev/null 654 + rm /tmp/test-direct*.txt /tmp/shared-*.txt /tmp/photo*.txt /tmp/empty* /tmp/kr-* /tmp/should-fail.txt /tmp/beach.jpg /tmp/notes.txt /tmp/sunset.jpg /tmp/deleteme.txt 2>/dev/null 526 655 527 656 # optionally logout test accounts 528 657 opake logout <B-handle>
+1
CHANGELOG.md
··· 51 51 - Fix missing HTTP status checks in XRPC client [#104](https://issues.opake.app/issues/104.html) 52 52 53 53 ### Changed 54 + - Update blackbox tests and docs for new commands [#159](https://issues.opake.app/issues/159.html) 54 55 - Add path-aware mv command [#157](https://issues.opake.app/issues/157.html) 55 56 - Add path-aware upload with directory placement [#158](https://issues.opake.app/issues/158.html) 56 57 - Add path-aware rm with recursive directory deletion [#156](https://issues.opake.app/issues/156.html)
+21 -4
README.md
··· 50 50 # upload a file (encrypts + uploads) 51 51 opake upload photo.jpg --tags vacation,beach 52 52 53 + # upload into a directory 54 + opake upload photo.jpg --dir Photos 55 + 56 + # organize files into directories 57 + opake mkdir Photos 58 + opake tree 59 + 53 60 # list your documents 54 61 opake ls 55 62 opake ls --long ··· 63 70 opake download photo.jpg 64 71 opake download photo.jpg -o ~/Downloads/copy.jpg 65 72 73 + # print a file to stdout (decrypt without saving) 74 + opake cat notes.txt 75 + opake cat Photos/notes.txt 76 + 66 77 # download a shared file from another user (via grant URI) 67 78 opake download --grant at://did:plc:abc/app.opake.cloud.grant/tid123 68 79 69 - # delete 80 + # delete (supports paths and recursive directory deletion) 70 81 opake rm photo.jpg 82 + opake rm Photos/photo.jpg 83 + opake rm -r Photos 84 + 85 + # move and rename 86 + opake mv photo.jpg Photos/ 87 + opake mv photo.jpg vacation-photo.jpg 71 88 72 89 # resolve a handle or DID to see their public key 73 90 opake resolve alice.example.com ··· 86 103 opake logout bob.other.com 87 104 ``` 88 105 89 - Commands accept either a filename or an `at://` URI. If a filename matches multiple documents, you'll be prompted to use the full URI. 106 + Commands accept a filename, a path (`Photos/beach.jpg`), or an `at://` URI. If a filename matches multiple documents, you'll be prompted to use the full URI. 90 107 91 - The `--as` flag works with document commands (`upload`, `download`, `ls`, `rm`, `share`, `shared`, `revoke`) and accepts a handle or DID. 108 + The `--as` flag works with document commands (`upload`, `download`, `ls`, `rm`, `mv`, `cat`, `tree`, `share`, `shared`, `revoke`) and accepts a handle or DID. 92 109 93 110 ## AppView 94 111 ··· 121 138 - [x] Grant listing (shared command) 122 139 - [x] AppView indexer (grants + keyrings from firehose) 123 140 - [x] AppView REST API with DID-scoped Ed25519 auth 141 + - [x] Folder hierarchy (mkdir, tree, path-aware rm/mv/cat/upload) 124 142 - [ ] Grant discovery (inbox command — queries AppView) 125 143 - [ ] Keyring-based group sharing 126 - - [ ] Folder hierarchy 127 144 - [ ] Web UI (SPA frontend) 128 145 129 146 ## Development