Share — known issues (revalidated)
Last Updated: 2026-05-20
Operational notes for drive/folder/file sharing, Shared with me, grantee self-removal, and receipt-sync pin failures. Revalidated against the tree on 2026-05-20 (not from memory).
Related: ADR 28, Swarm security gaps, E2E share.spec.ts, Share remediation plan (phased commits).
Primary gate (grantee E2E)
Success criterion for share remediation: multi-user grantee flow green before post-E2E hardening commits.
docker compose --profile gateway up -d --build --force-recreate gateway
pnpm run e2e:share -- --grep "grantee sees shared"Spec: share grantee (multi-user UI) in share.spec.ts — owner shares Personal Drive, folder, and file; grantee sees each in Shared with me; grantee removes self from each via the share panel.
| Status | Notes |
|---|---|
| In progress | Spec reaches drive/folder/file share and grantee removal; local runs can fail on gateway 502 during cargo-watch reloads or slow Postgres. Re-run after gateway listening and curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8080/api/v1/drives → 401. |
| Ingress (owner/grantee paths) | apply_grantee_* enqueue owner.access_removal_acknowledged; cargo test -p substratum-ingress --test grantee_acl_integration (2 tests) passes. Catalog ACL for Shared with me updates immediately via sync_passport_acl_* on owner/grantee PATCH (not only after worker). |
Treat this grep as the required gate for share work; see apps/file-explorer/e2e/AGENTS.md.
Pin authorization after receipt sync
Pin failures in docker logs (pin failed: unauthorized) were not caused by a bad pin JWT shape. The worker issues a short-lived owner pin JWT (GatewayPinPort → SwarmCommand::Pin); swarm validates it, then loads the passport receipt from the owner repo via PDS XRPC.
What happens on Pin
ReceiptSyncWorkerwritescloud.substratum.passport.receipton the owner repo (putRecord/ OAuth).- When
asset_cid_for_pinis set,GatewayPinPort::pin_blocksendsSwarmCommand::Pinwithcreate_pin_jwt(owner_did, …). LocalPassportAccessControlResolver::is_authorized:- Resolve owner repo base URL (
resolve_owner_pds_url). com.atproto.repo.getRecordon collectioncloud.substratum.passport.receiptwithreceipt_record_rkey(owner_did, asset_cid)(24-char hash, not the asset CID).- Confirm
receipt.asset_cidmatches the block CID. - Confirm JWT
sub(owner_didon the pin path) is owner or listed inaccess_control.
- Resolve owner repo base URL (
- If step 3’s
getRecordfails or ACL does not match →unauthorized/ pin denied.
Historical root causes — remediated in source (requires gateway rebuild)
| # | Symptom | Cause | Fix (in tree) |
|---|---|---|---|
| 1 | getRecord 404 / unauthorized for *.test users | Resolver used public AppView only; receipts live on local PDS (http://pds:3000) | Gateway wires HandleResolveConfig: try home PDS_URL, then ATPROTO_APPVIEW_URL, then PLC #atproto_pds (resolve_owner_pds_url in crates/auth/src/resolve.rs; apps/gateway/src/main.rs) |
| 2 | invalid record key / wrong rkey | RecordKey::new(asset_cid) (e.g. bafkreif…) | substratum_auth::receipt_record_rkey(owner_did, asset_cid) in crates/retrieval/src/access_control.rs (same as putRecord in passport-sync pds.rs) |
| 3 | Owners not on configured local PDS | No describeRepo / PLC fallback | resolve_owner_pds_url — describeRepo on PDS, then AppView, then DID doc |
Outdated summary: “Gateway passes only config.pds_url()” — replaced by full HandleResolveConfig (same routing idea as handle/profile resolution).
Cross-reference: Swarm security gaps §3 (pin PDS routing + replication-scoped JWT remediated in source).
Pin failure vs receipt-sync completion (revalidated)
Pin is not best-effort after PDS publish. In crates/passport-sync/src/worker.rs, pin_block runs before complete_upsert_receipt. A pin error returns Err → outbox retry or poison; catalog stays receipt_sync: pending until the job succeeds. Test: worker_schedules_retry_when_pin_fails.
Implication: repeated unauthorized Pin attempt lines usually mean the running container predates resolver fixes; after rebuild, pins should succeed once the receipt exists at the hashed rkey on the resolved PDS.
Deploy
docker compose --profile gateway up -d --build --force-recreate gatewayIf pin still fails after rebuild
- On owner PDS (or resolved host):
getRecordforcloud.substratum.passport.receiptwithreceipt_record_rkey(owner_did, asset_cid)— record must exist and match the job’s receipt CID. - Receipt
access_controlmust include the pin JWTsub(owner pin uses owner DID). - Postgres vs PDS: HTTP/catalog ACL may already list grantees while mesh still reads owner receipt — see eventual consistency below.
- Tail gateway logs on upload/share finalize and confirm
pinning blockacross targets instead ofunauthorized.
Eventual consistency (catalog, mesh, and UI)
| Plane | Authority | User-visible |
|---|---|---|
HTTP / explorer / GET /api/v1/shared-with-me | Postgres catalog (access_control, receipt_sync on drives/entries) | Share panel, Shared with me |
Mesh GetBlock / Pin | Owner-repo receipt via LocalPassportAccessControlResolver | Block fetch / replication after owner receipt converges |
After owner share or grantee self-removal PATCH, catalog updates immediately; owner-repo receipt and mesh ACL follow ReceiptSyncWorker (ADR 28). Until receipt_sync: synced, mesh may deny or allow differently than the UI suggests.
Grantee self-removal today (Phase 1): Grantee writes accessRemovalRequest on grantee PDS; owner receipt is updated async. Mesh ACL follows owner receipt only — bytes can remain mesh-readable briefly after the grantee removes themselves in the UI (ADR 28 § Mesh behavior).
Implemented (Phase 2b): Mesh denies non-owner requesters when a matching grantee-repo accessRemovalRequest exists (crates/retrieval/src/access_control.rs; retrieval tests).
Planned (Phase 1, optional): receipt_sync on GET /api/v1/shared-with-me (SharedItemDto) and explorer poll after share/removal — not in tree yet (SharedItemDto has no receipt_sync field).
E2E coverage
| Area | Status |
|---|---|
search-users (Bluesky + local *.test) | Covered |
| Owner share drive / folder / file (UI + API) | Covered |
API PATCH …/access-control on drive root | Covered |
| Grantee multi-user: Shared with me + remove self (drive, folder, file) | Spec present; required gate — see Primary gate |
Remediation roadmap (post-grantee E2E)
Ordered per share remediation plan. Status reflects the tree on 2026-05-20.
| Phase | Item | Status |
|---|---|---|
| 2d | accessRemovalAck on owner repo (audit chain after grantee removal) | Done — lexicon + passport-sync/ack.rs + worker path |
| 2e | Replication-scoped pin JWT (cid, owner_did, receipt_cid bound; not session JWT on libp2p) | Done — ReplicationPinClaims, receipt_sync_pin.rs, inbound replication verify |
| 2b | Mesh grantee-deny via grantee-repo accessRemovalRequest | Done — access_control.rs + wiremock tests |
| 2c | Share-spam ingress guards (ACL PATCH rate limit, grantee cap, orphan TTL) | Done — ingress acl_spam_guards; orphan TTL policy in ADR 28 |
| 1 | receipt_sync on shared-with-me + explorer poll | Deferred — catalog ACL sync makes listing immediate; add only if E2E still needs mesh/sync UX |
| 3 | Deferred — see below | Tracked; separate workstreams |
Phase 3 — deferred (separate ADRs / commits)
Do not block grantee E2E or Phases 2b–2e.
| Item | Notes |
|---|---|
| Orphan removal if owner never syncs | Documented TTL policy in ADR 28 (90d grantee intent / 30d poison outbox); automated cleanup not implemented |
| Home-base outbox | Gateway Postgres outbox only; SQLite/broker adapter per ADR 24/28 — installer-gui / home-base scope |
Mobile FFI pds_url | substratum-mobile-ffi uses pds_url = appview_url when unset — SaaS-only OK; home-base should pass real PDS_URL when available |
Quick reference (code)
| Concern | Location |
|---|---|
| Pin after sync | apps/gateway/src/receipt_sync_pin.rs, crates/passport-sync/src/worker.rs |
| PDS ACL on pin/get | crates/retrieval/src/access_control.rs |
| Owner PDS URL | crates/auth/src/resolve.rs (resolve_owner_pds_url, receipt_record_rkey) |
| Gateway wiring | apps/gateway/src/main.rs (HandleResolveConfig) |
| Share / grantee HTTP | crates/ingress/.../drives/mutations.rs |
| Shared with me API | crates/ingress/.../drives/mod.rs (GET /api/v1/shared-with-me) |