Skip to content

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.

bash
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.

StatusNotes
In progressSpec 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/drives401.
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 (GatewayPinPortSwarmCommand::Pin); swarm validates it, then loads the passport receipt from the owner repo via PDS XRPC.

What happens on Pin

  1. ReceiptSyncWorker writes cloud.substratum.passport.receipt on the owner repo (putRecord / OAuth).
  2. When asset_cid_for_pin is set, GatewayPinPort::pin_block sends SwarmCommand::Pin with create_pin_jwt(owner_did, …).
  3. LocalPassportAccessControlResolver::is_authorized:
    • Resolve owner repo base URL (resolve_owner_pds_url).
    • com.atproto.repo.getRecord on collection cloud.substratum.passport.receipt with receipt_record_rkey(owner_did, asset_cid) (24-char hash, not the asset CID).
    • Confirm receipt.asset_cid matches the block CID.
    • Confirm JWT sub (owner_did on the pin path) is owner or listed in access_control.
  4. If step 3’s getRecord fails or ACL does not match → unauthorized / pin denied.

Historical root causes — remediated in source (requires gateway rebuild)

#SymptomCauseFix (in tree)
1getRecord 404 / unauthorized for *.test usersResolver 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)
2invalid record key / wrong rkeyRecordKey::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)
3Owners not on configured local PDSNo describeRepo / PLC fallbackresolve_owner_pds_urldescribeRepo 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

bash
docker compose --profile gateway up -d --build --force-recreate gateway

If pin still fails after rebuild

  1. On owner PDS (or resolved host): getRecord for cloud.substratum.passport.receipt with receipt_record_rkey(owner_did, asset_cid) — record must exist and match the job’s receipt CID.
  2. Receipt access_control must include the pin JWT sub (owner pin uses owner DID).
  3. Postgres vs PDS: HTTP/catalog ACL may already list grantees while mesh still reads owner receipt — see eventual consistency below.
  4. Tail gateway logs on upload/share finalize and confirm pinning block across targets instead of unauthorized.

Eventual consistency (catalog, mesh, and UI)

PlaneAuthorityUser-visible
HTTP / explorer / GET /api/v1/shared-with-mePostgres catalog (access_control, receipt_sync on drives/entries)Share panel, Shared with me
Mesh GetBlock / PinOwner-repo receipt via LocalPassportAccessControlResolverBlock 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

AreaStatus
search-users (Bluesky + local *.test)Covered
Owner share drive / folder / file (UI + API)Covered
API PATCH …/access-control on drive rootCovered
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.

PhaseItemStatus
2daccessRemovalAck on owner repo (audit chain after grantee removal)Done — lexicon + passport-sync/ack.rs + worker path
2eReplication-scoped pin JWT (cid, owner_did, receipt_cid bound; not session JWT on libp2p)DoneReplicationPinClaims, receipt_sync_pin.rs, inbound replication verify
2bMesh grantee-deny via grantee-repo accessRemovalRequestDoneaccess_control.rs + wiremock tests
2cShare-spam ingress guards (ACL PATCH rate limit, grantee cap, orphan TTL)Done — ingress acl_spam_guards; orphan TTL policy in ADR 28
1receipt_sync on shared-with-me + explorer pollDeferred — catalog ACL sync makes listing immediate; add only if E2E still needs mesh/sync UX
3Deferred — see belowTracked; separate workstreams

Phase 3 — deferred (separate ADRs / commits)

Do not block grantee E2E or Phases 2b–2e.

ItemNotes
Orphan removal if owner never syncsDocumented TTL policy in ADR 28 (90d grantee intent / 30d poison outbox); automated cleanup not implemented
Home-base outboxGateway Postgres outbox only; SQLite/broker adapter per ADR 24/28 — installer-gui / home-base scope
Mobile FFI pds_urlsubstratum-mobile-ffi uses pds_url = appview_url when unset — SaaS-only OK; home-base should pass real PDS_URL when available

Quick reference (code)

ConcernLocation
Pin after syncapps/gateway/src/receipt_sync_pin.rs, crates/passport-sync/src/worker.rs
PDS ACL on pin/getcrates/retrieval/src/access_control.rs
Owner PDS URLcrates/auth/src/resolve.rs (resolve_owner_pds_url, receipt_record_rkey)
Gateway wiringapps/gateway/src/main.rs (HandleResolveConfig)
Share / grantee HTTPcrates/ingress/.../drives/mutations.rs
Shared with me APIcrates/ingress/.../drives/mod.rs (GET /api/v1/shared-with-me)