···83838484## Authentication
85858686-git-remote-pds supports two ways to authenticate with a PDS. Credentials are stored per-handle in `~/.config/pds-git-remote/auth.json`. If you have multiple accounts, each handle has its own credential — git-remote-pds uses the matching handle from the `pds://handle/repo` URL.
8686+git-remote-pds supports two ways to authenticate with a PDS. Credentials are stored per-handle in `~/.config/git-remote-pds/auth.json`. If you have multiple accounts, each handle has its own credential — git-remote-pds uses the matching handle from the `pds://handle/repo` URL.
87878888### OAuth login (recommended)
8989
+1-1
e2e-tests/pds-dev/README.md
···11# Local PDS for development
2233-Scripts for running an AT Protocol PDS server locally via Docker, used for integration testing of `pds-git-remote`.
33+Scripts for running an AT Protocol PDS server locally via Docker, used for integration testing of `git-remote-pds`.
4455## Quick start
66
+1-1
e2e-tests/pds-dev/run-e2e.sh
···4646# run E2E tests
4747echo ""
4848echo "=== Running E2E tests ==="
4949-cargo test -p pds-git-remote --features e2e -- --test-threads=1
4949+cargo test -p git-remote-pds --features e2e -- --test-threads=1
50505151echo ""
5252echo "=== All E2E tests passed ==="
+2-2
e2e-tests/playwright-tests/conftest.py
···11-"""Pytest fixtures for pds-git-remote OAuth playwright tests.
11+"""Pytest fixtures for git-remote-pds OAuth playwright tests.
2233Provides test configuration, binary location, PDS session tokens,
44and a shared Playwright browser context.
···6464@pytest.fixture(scope="session")
6565def auth_config_dir():
6666 """Create a temporary config directory for auth.json during tests."""
6767- tmpdir = tempfile.mkdtemp(prefix="pds-git-remote-test-config-")
6767+ tmpdir = tempfile.mkdtemp(prefix="git-remote-pds-test-config-")
6868 yield tmpdir
6969 # cleanup
7070 import shutil
+8-8
e2e-tests/playwright-tests/test_oauth_push.py
···11-"""End-to-end tests for pds-git-remote OAuth login and push.
11+"""End-to-end tests for git-remote-pds OAuth login and push.
2233Tests:
441. OAuth login via browser (Playwright automates PDS consent)
···145145146146 # verify credentials were stored
147147 auth_json_path = os.path.join(
148148- auth_config_dir, ".config", "pds-git-remote", "auth.json"
148148+ auth_config_dir, ".config", "git-remote-pds", "auth.json"
149149 )
150150 assert os.path.isfile(auth_json_path), (
151151 f"auth.json not found at {auth_json_path}"
···173173 repo_name = f"playwright-test-{int(time.time())}"
174174175175 # create a temp git repo with a test file
176176- repo_dir = tempfile.mkdtemp(prefix="pds-git-remote-push-test-")
176176+ repo_dir = tempfile.mkdtemp(prefix="git-remote-pds-push-test-")
177177 env = os.environ.copy()
178178 env["HOME"] = auth_config_dir
179179 env["PATH"] = debug_dir + ":" + env.get("PATH", "")
···214214 print(f"Push successful to {pds_remote}")
215215216216 # verify by cloning back with createSession token
217217- clone_dir = tempfile.mkdtemp(prefix="pds-git-remote-clone-test-")
217217+ clone_dir = tempfile.mkdtemp(prefix="git-remote-pds-clone-test-")
218218 clone_env = os.environ.copy()
219219 clone_env["PATH"] = debug_dir + ":" + clone_env.get("PATH", "")
220220 clone_env["PDS_ACCESS_TOKEN"] = pds_tokens["access_jwt"]
···257257 # read the stored auth.json and expire the token
258258 import json
259259 auth_json_path = os.path.join(
260260- auth_config_dir, ".config", "pds-git-remote", "auth.json"
260260+ auth_config_dir, ".config", "git-remote-pds", "auth.json"
261261 )
262262 assert os.path.isfile(auth_json_path), (
263263 f"auth.json not found — test_oauth_login must run first"
···282282283283 # create a repo and push — this should trigger token refresh
284284 repo_name = f"playwright-refresh-{int(time.time())}"
285285- repo_dir = tempfile.mkdtemp(prefix="pds-git-remote-refresh-test-")
285285+ repo_dir = tempfile.mkdtemp(prefix="git-remote-pds-refresh-test-")
286286 env = os.environ.copy()
287287 env["HOME"] = auth_config_dir
288288 env["PATH"] = debug_dir + ":" + env.get("PATH", "")
···354354 # create a unique repo name
355355 repo_name = f"playwright-incr-{int(time.time())}"
356356357357- repo_dir = tempfile.mkdtemp(prefix="pds-git-remote-incr-test-")
357357+ repo_dir = tempfile.mkdtemp(prefix="git-remote-pds-incr-test-")
358358 env = os.environ.copy()
359359 env["HOME"] = auth_config_dir
360360 env["PATH"] = debug_dir + ":" + env.get("PATH", "")
···409409 print("Incremental push successful")
410410411411 # verify by cloning back
412412- clone_dir = tempfile.mkdtemp(prefix="pds-git-remote-incr-clone-")
412412+ clone_dir = tempfile.mkdtemp(prefix="git-remote-pds-incr-clone-")
413413 clone_env = os.environ.copy()
414414 clone_env["PATH"] = debug_dir + ":" + clone_env.get("PATH", "")
415415 clone_env["PDS_ACCESS_TOKEN"] = pds_tokens["access_jwt"]
···11-# E2E Testing for pds-git-remote against Local PDS
11+# E2E Testing for git-remote-pds against Local PDS
2233## Context
4455-Phases 1-3.1 of pds-git-remote are implemented: core types, PDS client, bundle operations, push flow, and local PDS dev scripts. But the push tests only verify offline logic (empty repos, unreachable PDS, bundle creation). We need end-to-end tests that actually login to a PDS, upload blobs, write records, and verify the full push round-trip works.
55+Phases 1-3.1 of git-remote-pds are implemented: core types, PDS client, bundle operations, push flow, and local PDS dev scripts. But the push tests only verify offline logic (empty repos, unreachable PDS, bundle creation). We need end-to-end tests that actually login to a PDS, upload blobs, write records, and verify the full push round-trip works.
6677## Approach
8899### 1. Add `e2e` feature flag to Cargo.toml
10101111-**File:** `crates/pds-git-remote/Cargo.toml`
1111+**File:** `crates/git-remote-pds/Cargo.toml`
12121313Add a `[features]` section with a marker feature:
1414```toml
···20202121### 2. Create E2E test file
22222323-**File:** `crates/pds-git-remote/tests/e2e_tests.rs` (new)
2323+**File:** `crates/git-remote-pds/tests/e2e_tests.rs` (new)
24242525Gated with `#![cfg(feature = "e2e")]` at the module level.
2626···50501. `reset.sh` — clean slate
51512. `start.sh` — start PDS, wait for health check
52523. `create-account.sh` — create test account
5353-4. `cargo test -p pds-git-remote --features e2e -- --test-threads=1`
5353+4. `cargo test -p git-remote-pds --features e2e -- --test-threads=1`
54545. `stop.sh` on exit (via `trap`, unless `--keep` flag passed)
55555656Tests run serialized (`--test-threads=1`) since they share one PDS account.
···59596060| File | Action |
6161|------|--------|
6262-| `crates/pds-git-remote/Cargo.toml` | Add `[features]` section |
6363-| `crates/pds-git-remote/tests/e2e_tests.rs` | New: 5 E2E tests + helpers |
6262+| `crates/git-remote-pds/Cargo.toml` | Add `[features]` section |
6363+| `crates/git-remote-pds/tests/e2e_tests.rs` | New: 5 E2E tests + helpers |
6464| `scripts/pds-dev/run-e2e.sh` | New: harness script |
65656666## Verification
67676868-- `cargo test -p pds-git-remote --quiet` — existing 26 tests still pass (E2E tests not compiled)
6969-- `cargo test -p pds-git-remote --features e2e --quiet` — compiles E2E tests, skips gracefully if no PDS
6868+- `cargo test -p git-remote-pds --quiet` — existing 26 tests still pass (E2E tests not compiled)
6969+- `cargo test -p git-remote-pds --features e2e --quiet` — compiles E2E tests, skips gracefully if no PDS
7070- `./scripts/pds-dev/run-e2e.sh` — full E2E suite against real PDS (requires Docker)
7171- `cargo test -p lichen-cms -p lichen-server --quiet` — workspace unaffected
+5-5
plans/oauth-plan.md
···11-# OAuth Integration Plan for pds-git-remote
11+# OAuth Integration Plan for git-remote-pds
2233## Background
4455-pds-git-remote currently authenticates via AT Protocol's `com.atproto.server.createSession` endpoint (handle + app password). Credentials are stored as plaintext JSON in `~/.config/pds-git-remote/auth.json`. There is no token refresh logic — the stored `refresh_jwt` is never used. This works for development but has clear production shortcomings: plaintext secrets on disk, no session renewal, and requiring users to manage app passwords.
55+git-remote-pds currently authenticates via AT Protocol's `com.atproto.server.createSession` endpoint (handle + app password). Credentials are stored as plaintext JSON in `~/.config/git-remote-pds/auth.json`. There is no token refresh logic — the stored `refresh_jwt` is never used. This works for development but has clear production shortcomings: plaintext secrets on disk, no session renewal, and requiring users to manage app passwords.
6677This document explores two distinct integration angles inspired by git-credential-oauth, evaluates their complexity and tradeoffs, and recommends a sequencing strategy.
88···34343535### Concept
36363737-Instead of storing AT Protocol credentials in a custom `auth.json` file, store them through the standard Git credential helper infrastructure. This means pds-git-remote would call `git credential fill` / `git credential approve` / `git credential reject` to retrieve and persist tokens, leveraging whatever credential backend the user has configured (OS keychain, `git-credential-cache`, `pass`, etc.).
3737+Instead of storing AT Protocol credentials in a custom `auth.json` file, store them through the standard Git credential helper infrastructure. This means git-remote-pds would call `git credential fill` / `git credential approve` / `git credential reject` to retrieve and persist tokens, leveraging whatever credential backend the user has configured (OS keychain, `git-credential-cache`, `pass`, etc.).
38383939### What This Would Look Like
4040···85858686### Concept
87878888-Replace password-based `createSession` authentication with AT Protocol's native OAuth flow. Instead of users providing their handle and app password, pds-git-remote would open a browser, the user would authorize the application on their PDS, and pds-git-remote would receive scoped OAuth tokens.
8888+Replace password-based `createSession` authentication with AT Protocol's native OAuth flow. Instead of users providing their handle and app password, git-remote-pds would open a browser, the user would authorize the application on their PDS, and git-remote-pds would receive scoped OAuth tokens.
89899090### How AT Protocol OAuth Works
9191···108108109109### What It Would Require
110110111111-1. **Client metadata document** — Host a JSON file at a public HTTPS URL (e.g., `https://pds-git-remote.example.com/oauth-client-metadata.json`) describing the application. For native/CLI apps, a `http://127.0.0.1` redirect URI is standard. Alternatively, use the `did:web:` client ID scheme if available.
111111+1. **Client metadata document** — Host a JSON file at a public HTTPS URL (e.g., `https://git-remote-pds.example.com/oauth-client-metadata.json`) describing the application. For native/CLI apps, a `http://127.0.0.1` redirect URI is standard. Alternatively, use the `did:web:` client ID scheme if available.
1121121131132. **DPoP implementation** — Generate an ES256 keypair per session. Sign a DPoP proof JWT for every token request and every authenticated API call. Handle server-issued nonce rotation. This is the most complex piece.
114114
+5-5
plans/plan.md
···11-# pds-git-remote
11+# git-remote-pds
2233A Rust crate providing PDS-backed git remote functionality — uses AT Protocol PDS as a git backup backend via incremental bundles.
44···8899## Overview
10101111-`pds-git-remote` is a new crate in the lichen workspace (`crates/pds-git-remote`). It provides:
1111+`git-remote-pds` is a new crate in the lichen workspace (`crates/git-remote-pds`). It provides:
121213131. **A library** — core types, PDS API client, bundle operations, and push/pull logic that lichen-cms (and other crates) can call directly
14142. **A git remote helper binary** (`git-remote-pds`) — so developers can `git push pds main` from the CLI
···51515252Foundation layer — types that model the PDS state record and an HTTP client for the PDS XRPC API.
53535454-- [x] create crate skeleton (`crates/pds-git-remote`, add to workspace `Cargo.toml`)
5454+- [x] create crate skeleton (`crates/git-remote-pds`, add to workspace `Cargo.toml`)
5555- [x] define core types in `types.rs`:
5656 - `GitRef { name, sha }`
5757 - `BundleEntry { parts (blob CIDs), prerequisites, tips, total_size, created_at }`
···166166 - `push <src>:<dst>` → create bundle, upload, update state
167167- [x] implement CLI auth in `auth.rs`:
168168 - `pds-git auth login` → login via createSession, cache token locally
169169- - token storage in `~/.config/pds-git-remote/auth.json`
169169+ - token storage in `~/.config/git-remote-pds/auth.json`
170170 - env var auth (`PDS_ACCESS_TOKEN`/`PDS_DID` or `PDS_HANDLE`/`PDS_PASSWORD`)
171171- [x] add `clap`-based CLI for auth subcommands
172172- [x] end-to-end test: init repo, add remote, push, clone elsewhere, verify content matches
···184184 - `status()` → `{ ahead, behind, last_push }` — compare local vs remote state
185185 - `compact()` — replace bundle chain with single full bundle
186186- [ ] integrate with `lichen-git`:
187187- - `git_push` placeholder in `lichen-git/src/git.rs` can call into `pds-git-remote`
187187+ - `git_push` placeholder in `lichen-git/src/git.rs` can call into `git-remote-pds`
188188 - auto-commit flow triggers `PdsBackup::push()` on a debounce timer
189189- [ ] add settings support:
190190 - extend `lichen-git` settings or add `[pds_backup]` section to site config
+2-2
src/auth.rs
···11//! Credential storage and authentication resolution.
22//!
33-//! Stores AT Protocol credentials at `~/.config/pds-git-remote/auth.json`.
33+//! Stores AT Protocol credentials at `~/.config/git-remote-pds/auth.json`.
44//! Provides `resolve_auth` for the remote helper to find credentials from
55//! env vars or stored config.
66···5555 let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
5656 PathBuf::from(home)
5757 .join(".config")
5858- .join("pds-git-remote")
5858+ .join("git-remote-pds")
5959 .join("auth.json")
6060}
6161
+1-1
src/lib.rs
···11-//! pds-git-remote: PDS-backed git remote via incremental bundles.
11+//! git-remote-pds: PDS-backed git remote via incremental bundles.
22//!
33//! Uses AT Protocol PDS as a git backup backend. Stores repositories
44//! as chains of incremental git bundles uploaded as PDS blobs, tracked
+4-4
src/main.rs
···111111112112/// Handles `auth oauth-login` — browser-based OAuth login with DPoP.
113113async fn handle_oauth_login(handle: &str, pds_url: Option<&str>, port: u16) {
114114- if let Err(e) = pds_git_remote::oauth_login::oauth_login(handle, pds_url, port).await {
114114+ if let Err(e) = git_remote_pds::oauth_login::oauth_login(handle, pds_url, port).await {
115115 eprintln!("OAuth login failed: {}", e);
116116 std::process::exit(1);
117117 }
···132132 }
133133 };
134134135135- match pds_git_remote::auth::login_and_store(pds_url, handle, &password).await {
135135+ match git_remote_pds::auth::login_and_store(pds_url, handle, &password).await {
136136 Ok(cred) => {
137137 eprintln!("logged in as {} ({})", cred.handle, cred.did);
138138 }
···145145146146/// Handles `auth status` — prints stored credentials with details.
147147fn handle_status() {
148148- match pds_git_remote::auth::load_config() {
148148+ match git_remote_pds::auth::load_config() {
149149 Ok(config) => {
150150 if config.credentials.is_empty() {
151151 eprintln!("no stored credentials");
···198198199199/// Handles `auth logout` — removes stored credential.
200200fn handle_logout(handle: &str) {
201201- match pds_git_remote::auth::logout(handle) {
201201+ match git_remote_pds::auth::logout(handle) {
202202 Ok(true) => {
203203 eprintln!("logged out {}", handle);
204204 }
+5-5
src/remote_helper.rs
···44//! between standard git commands and the PDS-backed bundle storage.
55//! All diagnostic output goes to stderr; stdout is protocol-only.
6677-use pds_git_remote::auth;
88-use pds_git_remote::fetch::{download_and_apply, find_new_bundles, read_remote_state};
99-use pds_git_remote::identity;
1010-use pds_git_remote::pds_client::PdsClient;
1111-use pds_git_remote::push;
77+use git_remote_pds::auth;
88+use git_remote_pds::fetch::{download_and_apply, find_new_bundles, read_remote_state};
99+use git_remote_pds::identity;
1010+use git_remote_pds::pds_client::PdsClient;
1111+use git_remote_pds::push;
12121313/// Parses a `pds://handle/repo-name` URL into (handle, repo_name).
1414fn parse_pds_url(url: &str) -> Result<(String, String), String> {