@0xordek/git-me
v0.4.1
Published
Self-hosted Git LFS utility for Cloudflare Workers, R2, and Durable Objects.
Readme
git-me
A zero-config self-hosted Git LFS service for Cloudflare Workers, R2, Durable Objects, and KV-backed legacy-user migration.
Built as a TypeScript Cloudflare Worker.
Zero-Config Quick Start
Requires Node.js 22+ and a Cloudflare account. Wrangler is bundled as the deploy engine; users do not need to install or configure it.
npm install -g @0xordek/git-me
git-me worker deployThe command opens Cloudflare login, creates the Worker, R2 bucket, KV namespace, and Durable Object migration, generates the admin secret, stores it in the operating system credential store, checks /health, and saves a local profile.
Use --account-id <id> when Cloudflare login has more than one account. A profile name cannot be deployed twice; use another --profile <name> for another Worker.
Example output:
Deployed: https://git-me-abc.workers.dev
Profile: default
LFS URL: https://git-me-abc.workers.devAdd and manage users without copying Worker URLs or admin tokens:
git-me user add 0xordek --access write
git-me user list
git-me user list --json
git-me user delete 0xordekPasswords are read from a hidden prompt. For automation, use stdin; secrets are never accepted as command arguments:
printf '%s' "$LFS_PASSWORD" | git-me user add 0xordek --access write --password-stdin
git-me user delete 0xordek --yesIf the operating system credential store is unavailable, pass the admin secret explicitly with --token-stdin or --token-env.
Existing users created before user listing are added to the list after their next successful login; Durable Objects cannot enumerate them safely.
Quick Start (development)
git clone [email protected]:0xordek/git-me.git
cd git-me
npm ci
npm run checkManual Cloudflare Setup (development/legacy)
The zero-config path above is recommended for users. The manual setup remains useful for local Worker development and existing deployments.
Create storage resources:
wrangler r2 bucket create git-me-objects
wrangler kv namespace create git-me-metadataWrangler prints the KV namespace id. Create private deployment config, then replace its placeholder ID:
cp wrangler.example.toml wrangler.local.tomlwrangler.local.toml is ignored by Git. Before a real deploy, replace its placeholder KV namespace ID with your namespace ID. npm run deploy creates the AuthUser Durable Object migration.
Set the admin token as a secret:
wrangler secret put GITME_AUTH_TOKENTransfer Modes
proxy: default mode. The Worker handles Git LFS upload and download bytes through thePUT /objects/{oid}andGET /objects/{oid}endpoints.direct: opt-in download acceleration. Uploads still use the Worker so every object gets SHA-256 verification; downloads receive short-lived signed R2GETURLs.
Leave GITME_TRANSFER_MODE unset for proxy, or set it to direct to opt in to signed R2 downloads. Give direct mode a bucket-scoped, read-only R2 S3 API token.
Configuration
| Name | Required | Purpose |
|------|----------|---------|
| GITME_AUTH_TOKEN | Yes | Admin bearer token for user management and emergency LFS access |
| GITME_TRANSFER_MODE | No | proxy by default; set direct for signed R2 downloads |
| GITME_SIGNED_URL_TTL_SECONDS | No | Signed URL TTL for direct mode; defaults to 900 |
| GITME_R2_ACCOUNT_ID | Direct mode | Cloudflare account id for R2 S3-compatible signing |
| GITME_R2_ACCESS_KEY_ID | Direct mode | Read-only R2 API access key id |
| GITME_R2_SECRET_ACCESS_KEY | Direct mode | Read-only R2 API secret access key |
| GITME_R2_BUCKET_NAME | Direct mode | R2 bucket used in signed download URLs |
Use Wrangler secrets for sensitive values:
wrangler secret put GITME_R2_SECRET_ACCESS_KEYDeploy:
npm run deploy:dry
npm run deployGit LFS Client Setup
For the zero-config flow, use the profile-based commands above. The explicit URL/token form remains available for another machine or when the credential store is unavailable; pass secrets through environment variables or standard input, never command arguments:
read -rsp 'Admin token: ' GITME_ADMIN_TOKEN; echo
export GITME_ADMIN_TOKEN
read -rsp 'LFS password: ' GITME_LFS_PASSWORD; echo
printf '%s' "$GITME_LFS_PASSWORD" | npx @0xordek/git-me user add \
--target https://your-worker.workers.dev \
--token-env GITME_ADMIN_TOKEN \
--username alice \
--password-stdin \
--access write
unset GITME_ADMIN_TOKEN GITME_LFS_PASSWORDUse "read" for pull-only users and "write" for pull/push users. Delete access with:
read -rsp 'Admin token: ' GITME_ADMIN_TOKEN; echo
export GITME_ADMIN_TOKEN
npx @0xordek/git-me user delete \
--target https://your-worker.workers.dev \
--token-env GITME_ADMIN_TOKEN \
--username alice
unset GITME_ADMIN_TOKENConfigure a repo:
git lfs track "*.psd" "*.zip" "*.bin"
git add .gitattributes
git config lfs.url https://your-worker.workers.dev
git config lfs.http.https://your-worker.workers.dev.locksverify falseFor shared repositories, commit .lfsconfig so fresh clones use the Worker too:
git config -f .lfsconfig lfs.url https://your-worker.workers.dev
git add .lfsconfig
git commit -m "chore: configure git-me lfs"Then use git push and git pull as normal. On first LFS access, Git asks for username and password. Git Credential Manager stores it on the machine, so deleting and cloning the repo again usually does not ask while the worker host stays the same.
The admin bearer token also works for LFS as an emergency write credential, but normal users should use Basic auth users created through /admin/users/{username}.
If you do not want the CLI, the same operations are available through GET /admin/users, PUT /admin/users/{username}, and DELETE /admin/users/{username}.
Migrating Existing LFS Objects
Use the migration CLI from a local repository to copy objects from the current LFS server into git-me:
read -rsp 'Target token: ' GITME_TARGET_TOKEN; echo
export GITME_TARGET_TOKEN
npx @0xordek/git-me migrate --target https://your-worker.workers.dev --token-env GITME_TARGET_TOKEN --dry-run
unset GITME_TARGET_TOKENRun --dry-run first. It scans local Git LFS pointer files, deduplicates object IDs, and reports scanned, unique, migrated, skipped, and failed without transferring object bytes or writing Git config.
For a GitHub source, pass the source LFS URL explicitly when it is not already in git config lfs.url:
read -rsp 'Target token: ' GITME_TARGET_TOKEN; echo
export GITME_TARGET_TOKEN
npx @0xordek/git-me migrate \
--source-url https://github.com/OWNER/REPO.git/info/lfs \
--target https://your-worker.workers.dev \
--token-env GITME_TARGET_TOKEN \
--dry-run
unset GITME_TARGET_TOKENFor another Git LFS server, use its Batch API base URL as --source-url:
read -rsp 'Target token: ' GITME_TARGET_TOKEN; echo
export GITME_TARGET_TOKEN
npx @0xordek/git-me migrate \
--source-url https://source.example.com/repo.git/info/lfs \
--target https://your-worker.workers.dev \
--token-env GITME_TARGET_TOKEN \
--dry-run
unset GITME_TARGET_TOKENPrivate source repositories usually require an extra source header. Repeat --source-header-env for every environment variable containing a name: value header:
read -rsp 'Target token: ' GITME_TARGET_TOKEN; echo
export GITME_TARGET_TOKEN
read -rsp 'Source header: ' GITME_SOURCE_AUTH; echo
export GITME_SOURCE_AUTH
npx @0xordek/git-me migrate \
--source-url https://github.com/OWNER/PRIVATE-REPO.git/info/lfs \
--source-header-env GITME_SOURCE_AUTH \
--target https://your-worker.workers.dev \
--token-env GITME_TARGET_TOKEN \
--dry-run
unset GITME_TARGET_TOKEN GITME_SOURCE_AUTHAfter a successful real migration, add --write-config to update the repository's lfs.url to the target:
read -rsp 'Target token: ' GITME_TARGET_TOKEN; echo
export GITME_TARGET_TOKEN
npx @0xordek/git-me migrate \
--target https://your-worker.workers.dev \
--token-env GITME_TARGET_TOKEN \
--write-config
unset GITME_TARGET_TOKENSafety model: the CLI uses the generic Git LFS Batch API for source downloads and target uploads. Object bytes are streamed through temporary files named git-me-migrate-* in the OS temp directory, not buffered in memory, and each downloaded file must match the pointer SHA-256 OID before upload. Temporary files are removed after each object attempt, and --write-config only runs when the migration has no failures.
API Endpoints
| Method | Path | Purpose |
|--------|------|---------|
| POST | /objects/batch | Git LFS Batch API |
| PUT | /objects/{oid} | Upload object bytes |
| GET | /objects/{oid} | Download object bytes |
| PUT | /admin/users/{username} | Create or update LFS user |
| GET | /admin/users | List usernames and access levels |
| DELETE | /admin/users/{username} | Delete LFS user |
| GET | /health | Configuration health check |
Batch requests and error responses use application/vnd.git-lfs+json.
GET /health returns application/json.
Storage Layout
- R2 object key:
objects/<oid> - Temporary proxy-upload key prefix:
objects/.tmp/ - Durable Object: one
AuthUserinstance per normalized username, plus reservedadmin:usersinstance for the serialized admin index - KV user key:
user:<username>only during legacy SHA-256 credential upgrade
Development
npm ci
npm run check
wrangler dev --config wrangler.local.tomlSecurity
Do not commit auth tokens or R2 API secrets. Use wrangler secret put GITME_AUTH_TOKEN and wrangler secret put GITME_R2_SECRET_ACCESS_KEY.
proxy uploads stream through the Worker and must match their Git LFS SHA-256 OID before becoming readable. direct mode only signs R2 downloads; it never signs uploads. Use a bucket-scoped, read-only R2 credential for direct mode.
Existing deployments upgraded from a release that signed direct uploads must first set GITME_TRANSFER_MODE=proxy, audit every objects/<oid> object against its SHA-256 filename, and quarantine mismatches. Rotate the old R2 S3 API token before enabling direct downloads again. Objects without the v0.3 Worker verification marker always fall back to proxy downloads, even when direct mode is enabled.
New passwords use salted PBKDF2-SHA-256 records in Durable Objects. Existing KV SHA-256 records upgrade after one successful login. Deleted users leave a Durable Object tombstone, so stale KV reads cannot restore access. Authentication locks one client source for one username for one minute after five failed attempts in one minute.
GET /health checks configuration only. It does not prove R2, KV, or Durable Object availability. Failed requests emit a request ID in X-Request-Id; logs never include credentials or request bodies.
Report vulnerabilities through GitHub Security Advisories. See SECURITY.md.
Contributing
Small fixes and focused issues are welcome. See CONTRIBUTING.md.
License
MIT — see LICENSE.
