Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
22f484a
feat(settings): adds schema export, plaintext import, crypto core
wgordon17 Aug 26, 2026
dc5e586
feat(settings): adds encrypted credential export and import
wgordon17 Aug 26, 2026
6bc458e
feat(login): adds pre-auth settings import from the login screen
wgordon17 Aug 26, 2026
d016c53
fix(settings): harden import/export per multi-agent review
wgordon17 Aug 26, 2026
fd75914
docs(deploy): adds CREDENTIAL_NONCE_KV provisioning + unseal endpoint
wgordon17 Aug 26, 2026
7edb67d
docs(worker): clarifies single-use nonce is availability-bounded
wgordon17 Aug 26, 2026
7d358dd
fix(settings): harden credential import and share its state machine
wgordon17 Aug 27, 2026
d5a8172
test: reroute rAF through a microtask so Kobalte modal cleanup runs i…
wgordon17 Aug 27, 2026
93a446a
fix(jira): drop JIRA_CLIENT_ID requirement for API-token proxying
wgordon17 Aug 27, 2026
2798fc7
feat(settings): encode the one-time code as Crockford base32
wgordon17 Aug 27, 2026
5f74836
feat(settings): export curated, privacy-scrubbed view preferences
wgordon17 Aug 27, 2026
278ee9d
feat(settings): import and apply curated view preferences
wgordon17 Aug 27, 2026
e341ab4
feat(settings): replace export checkbox with an export dialog + singl…
wgordon17 Aug 28, 2026
f2df69e
fix(settings): clarify the single-use terminal error message
wgordon17 Aug 28, 2026
955acd8
docs(user-guide): update import/export for base32 code, export dialog…
wgordon17 Aug 28, 2026
e75abba
fix(settings): fully reset prior identity on import and cross-tab switch
wgordon17 Aug 28, 2026
40b5bba
test(auth): stop cross-tab storage listeners leaking fetches across t…
wgordon17 Aug 28, 2026
ff4fb06
fix(settings): harden the export dialog and view-preferences schema p…
wgordon17 Aug 28, 2026
0922aa7
docs(user-guide): note cross-tab reload on identity-switch import
wgordon17 Aug 28, 2026
aabffef
fix(auth): reload cross-tab passive tabs on identity change even when…
wgordon17 Aug 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 38 additions & 1 deletion DEPLOY.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ routing.
- Multi-user tracking, upstream repo discovery, monitor-all mode
- Repo pinning/reordering, themes, ignore system
- IndexedDB caching + ETag optimization
- Plaintext settings export and import (client-side only)
- MCP server (separate Node.js process, independent of Worker)

**What does NOT work without a backend:**
Expand All @@ -41,6 +42,10 @@ routing.
404 console errors on static hosts. Optionally remove the `report-uri` and
`report-to` directives if the noise is unwanted.
- **Jira token sealing** (planned) — requires server-side encryption
- **Encrypted-credentials export/import** — the credential seal/unseal endpoints
(`/api/proxy/seal`, `/api/proxy/unseal`) run only on the Worker. Plaintext settings
export and import still work on static-only deploys; only the optional
encrypted-credentials bundle needs the backend.

**Security note:** The `public/_headers` file sets Content-Security-Policy and other
security headers. Ensure your static host serves these headers — Cloudflare Pages,
Expand All @@ -64,6 +69,7 @@ PAT instead), and Turnstile is only used by the planned Jira integration.
2. **Update `wrangler.toml`** — Change `pattern = "gh.gordoncode.dev"` to your domain
3. **Set GitHub Actions secrets and variables** — See sections below
4. **Set Cloudflare Worker secrets** — See "Cloudflare Worker Secrets" section below. **Critical:** `ALLOWED_ORIGIN` must exactly match your deployment URL (e.g., `https://your-domain.example.com`). An incorrect value causes all API requests to fail with CORS errors.
5. **Provision the credential-nonce KV namespace** — See "Cloudflare KV Namespaces" section below. Required for the encrypted-credential import feature; skipping it makes every encrypted-credential import fail with a 503 (plaintext-config import still works).

**Verify configuration:** Run `pnpm validate:deploy` locally to check that all required
Cloudflare Worker secrets are set. In CI, the deploy workflow runs
Expand Down Expand Up @@ -150,13 +156,44 @@ wrangler secret put ALLOWED_ORIGIN
- `GITHUB_CLIENT_SECRET`: the Client Secret from your GitHub OAuth App
- `ALLOWED_ORIGIN`: `https://YOUR-DOMAIN` (e.g. `https://gh.gordoncode.dev`)

## Cloudflare KV Namespaces

The encrypted-credential import feature (`/api/proxy/unseal`) requires a KV namespace
bound as `CREDENTIAL_NONCE_KV`. It backs single-use consumption of credential-bundle
nonces, blocking an unseal-then-reseal renewal of a bundle's 30-day expiry. It holds only
opaque nonce values (never plaintext, ciphertext, or tokens), with a TTL bounded by each
bundle's own expiry window.

Create the production and preview namespaces:

```sh
wrangler kv namespace create CREDENTIAL_NONCE_KV
wrangler kv namespace create CREDENTIAL_NONCE_KV --preview
```

Each command prints an ID. Add the returned `id` and `preview_id` to the
`[[kv_namespaces]]` block in `wrangler.toml`, replacing the upstream placeholder values:

```toml
[[kv_namespaces]]
binding = "CREDENTIAL_NONCE_KV"
id = "<your-production-id>"
preview_id = "<your-preview-id>"
```

`pnpm validate:deploy` does **not** check KV bindings — it validates Worker secrets and
build-time env vars only — so provision this manually. If the binding is missing or
misconfigured, every encrypted-credential import fails closed with a **503**; the
plaintext-configuration import path is unaffected and still works.

## Worker API Endpoints

| Endpoint | Method | Purpose |
|----------|--------|---------|
| `/api/oauth/token` | POST | Exchange OAuth authorization code for permanent access token. |
| `/api/health` | GET | Health check. Returns `OK`. |
| `/api/proxy/seal` | POST | Encrypt an API token for client-side storage. Requires Turnstile + session. |
| `/api/proxy/seal` | POST | Encrypt (seal) an API token or credential-export bundle for client-side storage. Requires Turnstile + session. |
| `/api/proxy/unseal` | POST | Decrypt (unseal) an encrypted credential-export bundle during import. Reachable pre-authentication (Login-page import). Requires Turnstile + the `CREDENTIAL_NONCE_KV` binding (single-use nonce enforcement). |

### Token Storage Security

Expand Down
98 changes: 97 additions & 1 deletion docs/USER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,16 @@ GitHub Tracker is a dashboard that aggregates open issues, pull requests, and Gi
- [Bookmarking Jira Issues](#bookmarking-jira-issues)
- [Disconnecting](#disconnecting-jira)
- [Settings Reference](#settings-reference)
- [Exporting and Importing Settings](#exporting-and-importing-settings)
- [Exporting](#exporting)
- [View Preferences Are Included Too](#view-preferences-are-included-too)
- [Including Encrypted Credentials](#including-encrypted-credentials)
- [Importing on the Settings Page](#importing-on-the-settings-page)
- [Importing on the Login Page](#importing-on-the-login-page)
- [The One-Time Code Is Single-Use](#the-one-time-code-is-single-use)
- [Keep the Export File Safe](#keep-the-export-file-safe)
- [Jira Credentials and Staleness](#jira-credentials-and-staleness)
- [Same-Deployment Only](#same-deployment-only)
- [Troubleshooting](#troubleshooting)

---
Expand Down Expand Up @@ -693,7 +703,7 @@ Settings are saved automatically to `localStorage` and persist across sessions.

### View State Settings

These are UI preferences that persist across sessions but are not included in the exported config file.
These are UI preferences that persist across sessions in your browser. Most of them also travel with an exported settings file — see [View Preferences Are Included Too](#view-preferences-are-included-too) for exactly what's included and what isn't.

| Setting | Default | Description |
|---------|---------|-------------|
Expand All @@ -707,6 +717,92 @@ These are UI preferences that persist across sessions but are not included in th

---

## Exporting and Importing Settings

You can export your configuration to a JSON file and import it back later — on the same machine, a new machine, or an incognito window for testing. Import is available both from the Settings page (when you are already signed in) and from the Login page (before you sign in, so a single file can restore your settings *and* sign you in).

### Exporting

Go to **Settings > Data > Export** and click **Export**. A dialog opens with two choices:

- **Export config only** — downloads `github-tracker-settings.json` immediately: your full configuration (repositories, organizations, tracked users, tabs, refresh interval, notification preferences, Jira display settings, and everything else on the Settings page) plus your view preferences (below), with no credentials.
- **Export with encrypted credentials** — see [Including Encrypted Credentials](#including-encrypted-credentials).

A plaintext export (config only) contains **no credentials**. Your Atlassian email is also omitted (it is personally identifiable and not needed to restore the configuration). Importing a plaintext export restores your settings but does not sign you in — you still authenticate normally.

### View Preferences Are Included Too

Every export — plaintext or with credentials — also carries a curated set of view preferences. On import, these are restored automatically alongside your configuration, for whichever GitHub identity that import establishes:

- Jira custom sort order
- Expand/collapse state and pinned/locked repos, per tab
- Tab filters (including per-tab username filters) and custom tab filter values
- Dependency-group expansion state
- The Show PR Runs and Hide Dependency Dashboard toggles
- Your Ignored Items and Tracked Items lists

A few things are deliberately left out:

- Issue/PR **titles and URLs**, and a tracked Jira issue's **status** (e.g. "In Progress"), are never included. On the target machine, tracked GitHub issues/PRs pick up their real title again automatically once the app has fetched their repo's data; tracked Jira issues show a blank title and status, and Ignored items show a blank title, until you interact with them again (re-track or re-ignore) — tracking and ignoring still work either way, since both are keyed by a stable identifier (item ID or Jira key), not the title.
- What DOES travel for those lists — repo names, issue/PR numbers, Jira keys — is plaintext in the export file, the same as the rest of your configuration. So are your saved filter values (per-tab username filters, custom tab filter text).
- The **org/repo filter** at the top of the dashboard is not included — it's transient and resets on a new browser or session anyway.
- View preferences travel in the **plaintext** part of the export, never inside the encrypted credentials section — there's nothing secret in them.

### Including Encrypted Credentials

To migrate a full session (or set up an incognito test session) without re-authenticating, choose **Export with encrypted credentials** in the export dialog. Right above that choice, the dialog shows a warning: *"This is a one-time transfer, not a durable backup — the encrypted credentials can be imported once and expire in 30 days."* The export then also bundles your GitHub token and — if connected — your Jira credentials, encrypted so they can travel in the file safely.

When you export with credentials:

1. A **one-time code** is generated and shown in a dialog — 26 characters, shown in dashed groups for readability (`XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XX`). **This code is displayed only once.** Copy it and save it **separately from the export file** (for example, in a password manager) — you need *both* the file and the code to restore credentials, and it cannot be shown again or recovered later. When retyping it, case doesn't matter, and the letters I, L, and O are read as 1, 1, and 0 respectively, so a common misreading of the code still works.
2. The file downloads only *after* you acknowledge the code dialog. If you dismiss the dialog without acknowledging, nothing is downloaded.

The code never leaves your browser and is never written into the export file. The credentials inside the file are encrypted with it, so the file alone cannot reveal them.

The encrypted-credentials portion of an export **expires 30 days after it is created**. After that, the credentials can no longer be imported and you must re-export. The plaintext configuration in the same file always imports regardless of age.

### Importing on the Settings Page

Go to **Settings > Data > Import** and choose an export file.

- **Plaintext file (no credentials):** you are asked to confirm, then your current settings are replaced.
- **File with encrypted credentials:** you are prompted for the one-time code. Enter it (use the show/hide toggle to check a manually typed code), then confirm. Because you are already signed in, the Settings page **always** shows an identity confirmation — "This will sign you in as @username and replace your current settings" — before finishing. You can also choose **Continue without credentials** to import just the plaintext configuration.

### Importing on the Login Page

Before signing in, click **Import from backup** on the Login page and choose an export file.

- A file **without** encrypted credentials cannot sign you in. You are told to sign in normally first, then use Import on the Settings page to restore your configuration.
- A file **with** encrypted credentials prompts for the one-time code and then signs you in automatically. If this browser already has settings from prior use (you have completed onboarding or selected any repos/orgs), an identity confirmation is shown first. A genuinely fresh browser or incognito window skips the confirmation and goes straight to the dashboard.

If you have this app open in other browser tabs, importing credentials for a different GitHub identity reloads those tabs automatically so they reflect the new identity — any unsaved in-tab state there (such as a partially typed filter) is lost.

### The One-Time Code Is Single-Use

The encrypted-credentials portion of an export can be **decrypted only once — the moment you first submit the one-time code**, not once per successful import. That first submission is irreversible for that file. This means any of the following, *after* the first code submission, permanently consumes the credentials portion:

- declining the identity confirmation,
- the GitHub token turning out to be revoked or expired,
- reloading the page or closing the tab.

If any of these happen, you'll see a message like *"Couldn't restore credentials — this file may already have been used (single-use), or the code/file don't match. Re-export to try again."* The plaintext configuration still imports fine, but the credentials can no longer be restored from that file — **re-export to get a fresh, single-use file** for another migration, test session, or retry.

Entering a wrong code is **not** one of these. Your one-time code is never sent to the server — it's checked locally against the result of that first submission — so a typo is safely retryable in the same session: just retype it and submit again, as long as you have not already declined the confirmation, reloaded the page, or closed the tab.

### Keep the Export File Safe

Anyone who obtains the export file — **even without the one-time code** — can trigger that single, irreversible decryption step and thereby invalidate the file's credentials portion for the rest of its lifetime. This is an availability/griefing consideration, **not** a confidentiality break: the one-time code still protects the credentials' secrecy, and recovery is simply re-exporting. Treat the export file as sensitive, and re-export if you suspect it was exposed.

### Jira Credentials and Staleness

Jira **OAuth** credentials in an export often go stale quickly. Atlassian rotates refresh tokens on every refresh, and a signed-in machine refreshes roughly once an hour — so an exported Jira-OAuth credential is typically invalidated within about an hour of export. For a reliable Jira-OAuth migration, **re-export immediately before importing**, or simply **reconnect Jira on the target machine** after importing (the import degrades gracefully with a reconnect prompt rather than failing the whole import). Jira **API-token** mode and GitHub tokens do not rotate and are unaffected.

### Same-Deployment Only

Encrypted credentials are sealed by this deployment's server and can only be imported back into **the same deployment**. Credentials exported from `gh.gordoncode.dev` cannot be imported into a different deployment (a self-hosted fork or a different domain). Plaintext configuration is portable across deployments; credentials are not.

---

## Troubleshooting

**Items I expect to see are not showing up.**
Expand Down
Loading
Loading