https://github.com/viktor-shcherb/open-user-state
Your user will own their own data! This is a backend code that implements user state persistance into their own personal GitHub repository.
https://github.com/viktor-shcherb/open-user-state
aes-gcm cloudflare-workers github-oauth kv-storage personal-access-token state-management typescript vitest
Last synced: about 1 month ago
JSON representation
Your user will own their own data! This is a backend code that implements user state persistance into their own personal GitHub repository.
- Host: GitHub
- URL: https://github.com/viktor-shcherb/open-user-state
- Owner: viktor-shcherb
- License: cc0-1.0
- Created: 2025-07-16T09:28:50.000Z (3 months ago)
- Default Branch: master
- Last Pushed: 2025-07-16T17:00:13.000Z (3 months ago)
- Last Synced: 2025-07-17T13:55:44.979Z (3 months ago)
- Topics: aes-gcm, cloudflare-workers, github-oauth, kv-storage, personal-access-token, state-management, typescript, vitest
- Language: TypeScript
- Homepage:
- Size: 107 KB
- Stars: 1
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
- Security: docs/security.md
Awesome Lists containing this project
README
# Open User State Backend
[](https://codecov.io/gh/viktor-shcherb/open-user-state)
[](https://open-user-state-personal-website.viktoroo-sch.workers.dev/api/health)This project hosts a Cloudflare Worker that serves as a backend for
[frontend](https://github.com/viktor-shcherb/viktor-shcherb.github.io). The aim
is to authenticate users via GitHub and sync editor state to a repository using
a Personal Access Token (PAT).The code is split into small modules under `src/`:
`auth.ts` handles OAuth and token encryption, `repo.ts` manages repository
preferences and `files.ts` wraps the GitHub file APIs. `index.ts` wires these
together and exposes the HTTP routes.## Development
Install dependencies and start a local dev server:
```bash
npm install
npm run dev
```Run the test suite with coverage:
```bash
npm run coverage
```Vitest expects a Node runtime that exposes the standard `webcrypto` API on
`globalThis.crypto` (Node 18+). When running on older versions the tests will
shim the API automatically.## OAuth & State Storage
Users authenticate via GitHub OAuth. The worker stores a short session
identifier in `SESSIONS` once the callback exchange succeeds. Subsequent
requests use this cookie to look up the GitHub `login` and numeric `id`.A separate `POST /api/token` call persists a fine grained PAT encrypted in the
`USER_PAT_STORE` namespace. Repository preferences are saved per user in
`USER_REPO_STORE`. Editor state is committed as plain text files; each file is
written individually and overwrites any existing blob at that path.## API Endpoints
The worker exposes a small set of routes used by the frontend to authenticate
with GitHub, store a Personal Access Token (PAT) and manage the repository
where user state is kept.
### `GET /api/health`Returns a small JSON payload `{ status: 'ok' }` which can be used by the
frontend to verify that the backend is running. A `HEAD` request returns the
same headers without the body.### `GET|POST /api/auth/github`
Initiates the OAuth login flow. The backend responds with a redirect to GitHub
and accepts both `GET` and `POST` methods. From the browser you can trigger the
flow with:```ts
await fetch('/api/auth/github', { method: 'POST', credentials: 'include' })
.then(res => {
if (res.redirected) window.location.href = res.url;
});
```### `GET /api/auth/github/callback`
GitHub redirects back to this route after the user approves the OAuth request.
The worker exchanges the `code` parameter for a short‑lived access token,
creates a session cookie, and then redirects the user to `/`.
This endpoint is handled automatically as part of the OAuth redirect and does
not need to be called manually from the frontend.### `POST /api/token`
Stores a fine‑grained PAT for the authenticated user. The request must include
the session cookie set during OAuth login.```ts
await fetch('/api/token', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ pat }),
});
```The token will be encrypted and stored securely in the worker's `user-pat-store`
KV namespace.### `DELETE /api/token`
Removes the stored PAT for the current session. Subsequent file or repository
operations will fail until a new token is provided.### `POST /api/logout`
Clears the active session cookie on the server so subsequent requests are
unauthenticated.### `POST /api/repository`
Persists the GitHub repository where user state files will be written. The body
must include `{ repo: 'owner/name' }` and the request requires a valid session
cookie.### `GET /api/repository`
Returns the currently selected repository for the authenticated user in the form
`{ repo: string }`.### `GET /api/profile`
Returns `{ username, avatar, patValid, repo }` for the authenticated user.
`patValid` indicates whether the stored PAT successfully fetches the account
information from GitHub.### `POST /api/file`
Commits a text file to the selected repository. The JSON payload should include
`path`, `content` and optionally a commit `message`. The file is created if it
does not exist or overwritten when the contents differ. The repository will be
created automatically if missing.### `GET /api/file`
Retrieves the raw text at the given `path` from the selected repository. The
path is provided as a query parameter. When the file or repository is missing
the response is `404`.### `GET /api/files`
Lists the entries under a directory in the selected repository. Pass the `path`
query parameter to specify the directory (or omit for the repo root). The
response is an array like `{ files: string[] }` containing file and folder
names.## Error Codes
All error responses use the JSON shape `{ "error": "CODE" }`. Consult
[docs/errors.md](docs/errors.md) for a list of possible codes and the routes
that may produce them.