diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 771e9a2..d5164f5 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -7,34 +7,79 @@ on:
branches: [ main, master ]
jobs:
+ lint:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v6
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v6
+ with:
+ node-version: '18'
+ cache: 'npm'
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Run linter
+ run: npm run lint
+
+ typecheck:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v6
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v6
+ with:
+ node-version: '18'
+ cache: 'npm'
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Run type checker
+ run: npm run typecheck
+
test:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v6
- - name: Setup Node.js
- uses: actions/setup-node@v6
- with:
- node-version: '18'
- cache: 'npm'
+ - name: Setup Node.js
+ uses: actions/setup-node@v6
+ with:
+ node-version: '18'
+ cache: 'npm'
- - name: Install dependencies
- run: npm ci
+ - name: Install dependencies
+ run: npm ci
- - name: Run linter
- run: npm run lint
+ - name: Run tests with coverage
+ run: npm test -- --coverage --coverageReporters=text --coverageReporters=lcov
- - name: Run type checker
- run: npm run typecheck
+ - name: Upload coverage reports to Codecov
+ uses: codecov/codecov-action@v5
+ with:
+ token: ${{ secrets.CODECOV_TOKEN }}
+
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v6
- - name: Run tests with coverage
- run: npm test -- --coverage --coverageReporters=text --coverageReporters=lcov
+ - name: Setup Node.js
+ uses: actions/setup-node@v6
+ with:
+ node-version: '18'
+ cache: 'npm'
- - name: Upload coverage reports to Codecov
- uses: codecov/codecov-action@v5
- with:
- token: ${{ secrets.CODECOV_TOKEN }}
+ - name: Install dependencies
+ run: npm ci
- - name: Validate extension build
- run: npm run build
+ - name: Validate extension build
+ run: npm run build
diff --git a/PRIVACY.md b/PRIVACY.md
index b879d80..021aedb 100644
--- a/PRIVACY.md
+++ b/PRIVACY.md
@@ -12,9 +12,10 @@ GitHub Devwatch is a Chrome extension for monitoring activity on GitHub reposito
GitHub Devwatch collects and stores the following data **locally on your device only**:
-1. **GitHub Personal Access Token**
+1. **GitHub OAuth Session**
+ - Created when you connect GitHub through the built-in device-flow sign-in
- Stored by the extension in Chrome storage
- - Current builds encrypt the token before writing it to local storage and keep a decrypted session copy while the extension is running
+ - Current builds encrypt the auth session before writing it to local storage and keep a decrypted session copy while the extension is running
- Used only to authenticate with GitHub's API
- Not sent to third-party services operated by this project
- Never shared with anyone
@@ -46,16 +47,16 @@ GitHub Devwatch collects and stores the following data **locally on your device
All data collected is used exclusively to provide the extension's functionality:
-- Your GitHub token authenticates API requests to GitHub
+- Your GitHub connection authenticates API requests to GitHub
- Your repository list determines which repositories to monitor
- Your settings customize how the extension behaves
- Activity data is displayed in the extension popup for your review
## Data Storage
-- The extension uses Chrome storage APIs for settings, cached activity, and token handling
+- The extension uses Chrome storage APIs for settings, cached activity, and GitHub sign-in handling
- Settings and repository lists can optionally sync across your Chrome browsers if you use Chrome Sync
-- Token handling uses local and session storage rather than Chrome sync
+- GitHub sign-in data uses local and session storage rather than Chrome sync
- You can clear all data at any time by uninstalling the extension or using Chrome's "Clear extension data" feature
## Third-Party Services
@@ -65,7 +66,7 @@ All data collected is used exclusively to provide the extension's functionality:
This extension communicates with GitHub's API (api.github.com) to fetch repository activity. When you use this extension:
- API requests are made directly from your browser to GitHub
-- Requests include your GitHub Personal Access Token for authentication
+- Requests include your GitHub OAuth access token for authentication
- GitHub's privacy policy and terms of service apply to these interactions
- See GitHub's privacy policy at: https://docs.github.com/en/site-policy/privacy-policies/github-privacy-statement
@@ -90,7 +91,7 @@ GitHub Devwatch does **NOT**:
The extension requests the following Chrome permissions:
-- **storage**: To save your settings, token, and activity data locally
+- **storage**: To save your settings, GitHub sign-in state, and activity data locally
- **alarms**: To periodically check for new repository activity
- **notifications**: To show you browser notifications for new activity
- **Host permission for api.github.com**: To fetch repository activity from GitHub's API
@@ -104,14 +105,14 @@ You have complete control over your data:
- **View Your Data**: All settings are visible in the extension's options page
- **Delete Your Data**: Uninstall the extension to remove all data, or use the "Clear All Data" option in settings
- **Export Your Data**: Use the backup/restore feature to export your settings
-- **Revoke Access**: Remove or regenerate your GitHub Personal Access Token at any time via GitHub's settings
+- **Revoke Access**: Disconnect locally in DevWatch, and revoke the OAuth app in GitHub's authorized applications settings at any time
## Security
Current builds include several concrete safeguards:
- All API requests use HTTPS
-- The token is encrypted before it is persisted locally
+- The GitHub auth session is encrypted before it is persisted locally
- The codebase includes input sanitization and GitHub URL validation checks
- Extension pages use a Content Security Policy
diff --git a/README.md b/README.md
index b8daeb1..121767e 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@ Monitor pull requests, issues, and releases across multiple GitHub repositories
## Key Features
-- **Guided Setup** - Built-in setup flow for token creation and repository selection
+- **Guided Setup** - Built-in GitHub sign-in flow and repository selection
- **Browser Notifications** - Get notified about new PRs, issues, and releases
- **Multi-Repo Monitoring** - Watch up to 50 repositories from one interface
- **Configurable Updates** - Check every 5, 15, 30, or 60 minutes
@@ -30,7 +30,7 @@ Monitor pull requests, issues, and releases across multiple GitHub repositories
3. Grant permissions when prompted
4. Follow the guided setup wizard on first launch
-**GitHub Token Permissions**: You'll need a [Personal Access Token](https://github.com/settings/tokens/new) with `repo` (for private repos) or `public_repo` (for public only).
+**GitHub Sign-In Permissions**: DevWatch uses GitHub OAuth device flow and requests `repo` plus `read:user` so it can monitor private repositories and show the connected account in the UI.
### Manual Installation (For Development)
@@ -53,7 +53,7 @@ cd devwatch-github
### First-Time Setup
The built-in setup flow walks you through:
-1. Create a GitHub token
+1. Connect your GitHub account
2. Add repositories to watch
3. Choose activity types (PRs, Issues, Releases)
@@ -74,7 +74,7 @@ The built-in setup flow walks you through:
Filter by type (All/PRs/Issues/Releases), search activities, refresh manually, or browse the archive. Click any item to open in GitHub.
### Settings Page
-Manage your GitHub token, watched repositories, activity filters, check interval, notifications, and theme. Export/import settings for backup.
+Manage your GitHub connection, watched repositories, activity filters, check interval, notifications, and theme. Export/import settings for backup.
@@ -101,7 +101,7 @@ That said, this project has not gone through a formal accessibility audit or doc
## Privacy & Security Notes
-The extension talks directly to GitHub's API and does not use a separate analytics or sync backend. It stores settings and cached activity in Chrome extension storage, and the current build encrypts the GitHub token before persisting it locally while keeping a decrypted session copy available at runtime.
+The extension talks directly to GitHub's API and does not use a separate analytics or sync backend. It stores settings and cached activity in Chrome extension storage, and the current build encrypts the GitHub auth session before persisting it locally while keeping a decrypted session copy available at runtime.
- **Direct network access** - Requests go to `api.github.com`, plus `registry.npmjs.org` only when you use package-name lookup
- **Scoped browser permissions** - The manifest asks for `storage`, `alarms`, and `notifications`
diff --git a/SECURITY.md b/SECURITY.md
index 044cb0e..a8193f9 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -15,7 +15,7 @@ I'll respond within 48 hours and work with you to understand and address the iss
Things I want to know about:
- XSS vulnerabilities or ways to inject malicious code
-- Token leakage or insecure storage
+- OAuth session leakage or insecure storage
- Ways to access other users' data
- Privilege escalation
- Dependencies with known CVEs
@@ -32,8 +32,8 @@ These are better suited for regular issues:
The extension includes several concrete protections, but this project has not been through a formal external security audit.
-### Token Storage
-- GitHub tokens are encrypted before they are written to local extension storage
+### GitHub Sign-In Storage
+- GitHub auth sessions are encrypted before they are written to local extension storage
- A decrypted copy may be cached in session storage while the extension is running
- Never transmitted to third-party servers
@@ -49,7 +49,7 @@ The extension includes several concrete protections, but this project has not be
### API Security
- All requests use HTTPS
-- Tokens are included in headers, never in URLs
+- OAuth access tokens are included in headers, never in URLs
- Rate limiting is respected to prevent abuse
## Supported Versions
diff --git a/background.js b/background.js
index f687fb5..24dc655 100644
--- a/background.js
+++ b/background.js
@@ -1,5 +1,5 @@
import { createHeaders, handleApiResponse, mapActivity, filterActivitiesByDate } from './shared/github-api.js';
-import { getSyncItems, getLocalItems, setLocalItem, getExcludedRepos, getToken, getFilteringSettings } from './shared/storage-helpers.js';
+import { getSyncItems, getLocalItems, setLocalItem, getExcludedRepos, getAccessToken, getFilteringSettings, getWatchedRepos } from './shared/storage-helpers.js';
import { extractRepoName } from './shared/repository-utils.js';
import { safelyOpenUrl } from './shared/security.js';
@@ -73,11 +73,10 @@ if (typeof chrome !== 'undefined' && chrome.notifications) {
async function checkGitHubActivity() {
try {
- // Get token from secure local storage
- const githubToken = await getToken();
+ const githubToken = await getAccessToken();
- const { watchedRepos, lastCheck, filters, notifications, mutedRepos, snoozedRepos, unmutedRepos } = await getSyncItems([
- 'watchedRepos',
+ const watchedRepos = await getWatchedRepos();
+ const { lastCheck, filters, notifications, mutedRepos, snoozedRepos, unmutedRepos } = await getSyncItems([
'lastCheck',
'filters',
'notifications',
@@ -87,7 +86,7 @@ async function checkGitHubActivity() {
]);
if (!githubToken) {
- console.warn('[DevWatch] No GitHub token found. Please add a token in settings.');
+ console.warn('[DevWatch] No GitHub connection found. Please connect GitHub in settings.');
return;
}
@@ -287,7 +286,7 @@ async function fetchRepoActivity(repo, token, since, filters) {
// Store error for user display but don't crash
let userMessage = 'Unable to fetch repository activity';
if (error.message.includes('401')) {
- userMessage = 'Authentication failed. Please check your GitHub token.';
+ userMessage = 'GitHub sign-in expired or was revoked. Reconnect GitHub in settings.';
} else if (error.message.includes('403')) {
userMessage = 'Access denied or rate limit exceeded.';
} else if (error.message.includes('404')) {
diff --git a/manifest.json b/manifest.json
index 3b9504f..1c7478f 100644
--- a/manifest.json
+++ b/manifest.json
@@ -10,7 +10,8 @@
"notifications"
],
"host_permissions": [
- "https://api.github.com/*"
+ "https://api.github.com/*",
+ "https://github.com/*"
],
"background": {
"service_worker": "background.js",
@@ -33,6 +34,6 @@
"128": "icons/icon128.png"
},
"content_security_policy": {
- "extension_pages": "script-src 'self'; object-src 'self'; connect-src 'self' https://api.github.com https://registry.npmjs.org; img-src 'self' https: data:; default-src 'self'; style-src 'self'"
+ "extension_pages": "script-src 'self'; object-src 'self'; connect-src 'self' https://api.github.com https://github.com https://registry.npmjs.org; img-src 'self' https: data:; default-src 'self'; style-src 'self'"
}
}
diff --git a/options/controllers/export-import-controller.js b/options/controllers/export-import-controller.js
index 5740b07..1267cad 100644
--- a/options/controllers/export-import-controller.js
+++ b/options/controllers/export-import-controller.js
@@ -1,16 +1,18 @@
import { NotificationManager } from '../../shared/ui/notification-manager.js';
+import { getWatchedRepos, setWatchedRepos } from '../../shared/storage-helpers.js';
const notifications = NotificationManager.getInstance();
export async function exportSettings() {
try {
const syncData = await chrome.storage.sync.get(null);
+ const watchedRepos = await getWatchedRepos();
const exportData = {
version: '1.0.0',
exportedAt: new Date().toISOString(),
settings: {
- watchedRepos: syncData.watchedRepos || [],
+ watchedRepos,
mutedRepos: syncData.mutedRepos || [],
pinnedRepos: syncData.pinnedRepos || [],
filters: syncData.filters || { prs: true, issues: true, releases: true },
@@ -53,7 +55,7 @@ export async function handleImportFile(event, loadSettingsCallback) {
}
const confirmed = confirm(
- 'This will replace your current settings (except GitHub token). Continue?'
+ 'This will replace your current settings (except your GitHub connection). Continue?'
);
if (!confirmed) {
@@ -63,8 +65,8 @@ export async function handleImportFile(event, loadSettingsCallback) {
}
const settings = importData.settings;
+ await setWatchedRepos(settings.watchedRepos || []);
await chrome.storage.sync.set({
- watchedRepos: settings.watchedRepos || [],
mutedRepos: settings.mutedRepos || [],
pinnedRepos: settings.pinnedRepos || [],
filters: settings.filters || { prs: true, issues: true, releases: true },
diff --git a/options/controllers/import-controller.js b/options/controllers/import-controller.js
index d355295..4f2dd3f 100644
--- a/options/controllers/import-controller.js
+++ b/options/controllers/import-controller.js
@@ -1,4 +1,5 @@
-import { getToken } from '../../shared/storage-helpers.js';
+import { STORAGE_CONFIG } from '../../shared/config.js';
+import { getAccessToken, getSyncItem, setWatchedRepos } from '../../shared/storage-helpers.js';
import { createHeaders } from '../../shared/github-api.js';
import { escapeHtml, unescapeHtml } from '../../shared/sanitize.js';
import { formatDateVerbose } from '../../shared/utils.js';
@@ -61,8 +62,27 @@ function formatNumber(num) {
return num.toString();
}
+function getRepoFullName(repo) {
+ if (typeof repo === 'string') {
+ return repo;
+ }
+
+ return repo?.fullName || '';
+}
+
+function normalizeImportedRepo(repo) {
+ return {
+ fullName: repo.fullName,
+ description: repo.description || 'No description provided',
+ language: repo.language || 'Unknown',
+ stars: repo.stars || 0,
+ updatedAt: repo.updatedAt || new Date().toISOString(),
+ addedAt: new Date().toISOString()
+ };
+}
+
export async function openImportModal(type, watchedRepos) {
- const token = await getToken();
+ const token = await getAccessToken();
if (!token) {
return;
}
@@ -99,7 +119,9 @@ export async function openImportModal(type, watchedRepos) {
const repos = await fetchReposFromGitHub(type, token);
const alreadyAdded = new Set(
- (watchedRepos || []).map(r => r.fullName.toLowerCase())
+ (watchedRepos || [])
+ .map(repo => getRepoFullName(repo).toLowerCase())
+ .filter(Boolean)
);
importModalState.repos = repos.map(repo => ({
@@ -162,7 +184,7 @@ async function fetchReposFromGitHub(type, token) {
if (!response.ok) {
if (response.status === 401) {
- throw new Error('Invalid GitHub token');
+ throw new Error('GitHub sign-in expired or was revoked');
} else if (response.status === 403) {
throw new Error('Rate limit exceeded or insufficient permissions');
} else {
@@ -207,6 +229,12 @@ export function closeImportModal() {
const modal = document.getElementById('importModal');
modal.classList.remove('show');
document.getElementById('importRepoSearch').value = '';
+ const selectAllCheckbox = document.getElementById('selectAllImport');
+ if (selectAllCheckbox) {
+ selectAllCheckbox.checked = false;
+ }
+ document.getElementById('selectedCount').textContent = '0';
+ document.getElementById('confirmImportBtn').disabled = true;
modal.removeEventListener('keydown', handleModalFocusTrap);
@@ -311,14 +339,36 @@ export async function importSelectedRepos(watchedRepos, onReposAdded) {
return;
}
- const reposWithTimestamp = reposToImport.map(repo => ({
- ...repo,
- addedAt: new Date().toISOString()
- }));
+ const existingRepoNames = new Set(
+ (watchedRepos || [])
+ .map(repo => getRepoFullName(repo).toLowerCase())
+ .filter(Boolean)
+ );
+ const uniqueReposToImport = reposToImport.filter(repo => !existingRepoNames.has(repo.fullName.toLowerCase()));
+
+ if (uniqueReposToImport.length === 0) {
+ return;
+ }
+
+ const allowUnlimitedRepos = await getSyncItem('allowUnlimitedRepos', false);
+
+ if (!allowUnlimitedRepos) {
+ const remainingSlots = Math.max(STORAGE_CONFIG.MAX_WATCHED_REPOS - existingRepoNames.size, 0);
+
+ if (uniqueReposToImport.length > remainingSlots) {
+ throw new Error(
+ `Import would exceed the ${STORAGE_CONFIG.MAX_WATCHED_REPOS} repository limit. Select ${remainingSlots} or fewer repositories, or enable "Unlimited Repositories" in Advanced settings.`
+ );
+ }
+ }
- watchedRepos.push(...reposWithTimestamp);
+ const nextWatchedRepos = [
+ ...watchedRepos,
+ ...uniqueReposToImport.map(normalizeImportedRepo)
+ ];
- await chrome.storage.sync.set({ watchedRepos });
+ await setWatchedRepos(nextWatchedRepos);
+ watchedRepos.splice(0, watchedRepos.length, ...nextWatchedRepos);
if (onReposAdded) {
onReposAdded();
diff --git a/options/controllers/token-controller.js b/options/controllers/token-controller.js
index 3d3d6bf..07fd82f 100644
--- a/options/controllers/token-controller.js
+++ b/options/controllers/token-controller.js
@@ -1,138 +1,164 @@
-import { createHeaders } from '../../shared/github-api.js';
-import { clearToken as clearStoredToken } from '../../shared/storage-helpers.js';
+import { completeGitHubDeviceAuth } from '../../shared/auth.js';
+import {
+ clearAuthSession,
+ getAuthSession,
+ setAuthSession
+} from '../../shared/storage-helpers.js';
+import { OAUTH_CONFIG } from '../../shared/config.js';
import { NotificationManager } from '../../shared/ui/notification-manager.js';
const notifications = NotificationManager.getInstance();
-export async function clearToken() {
- if (!confirm('Are you sure you want to clear your GitHub token?')) {
- return false;
- }
-
- document.getElementById('githubToken').value = '';
- document.getElementById('tokenStatus').textContent = '';
- document.getElementById('tokenStatus').className = 'token-status';
- document.getElementById('clearTokenBtn').style.display = 'none';
-
+function setRepoAccessState(isConnected) {
const repoInput = document.getElementById('repoInput');
- repoInput.disabled = true;
- repoInput.placeholder = 'Enter a valid GitHub token to add repositories';
- document.getElementById('addRepoBtn').disabled = true;
- document.getElementById('repoHelpText').textContent = 'Add a valid GitHub token above to start adding repositories';
+ const addRepoBtn = document.getElementById('addRepoBtn');
+ const repoHelpText = document.getElementById('repoHelpText');
+ const importSection = document.getElementById('importReposSection');
+
+ repoInput.disabled = !isConnected;
+ repoInput.placeholder = isConnected
+ ? 'e.g., react, facebook/react, or GitHub URL'
+ : 'Connect GitHub to add repositories';
+ addRepoBtn.disabled = !isConnected;
+ repoHelpText.textContent = isConnected
+ ? 'Add repositories to monitor (npm package, owner/repo, or GitHub URL)'
+ : 'Connect GitHub above to start adding repositories';
+ importSection.classList.toggle('hidden', !isConnected);
+ importSection.style.display = isConnected ? 'block' : 'none';
+}
- document.getElementById('importReposSection').style.display = 'none';
+function setDeviceCode(userCode = '') {
+ const deviceCodeInput = document.getElementById('githubToken');
+ const deviceCodeSection = document.getElementById('deviceCodeSection');
- await clearStoredToken();
+ if (!deviceCodeInput || !deviceCodeSection) {
+ return;
+ }
- notifications.info('GitHub token cleared successfully');
- return true;
+ deviceCodeInput.value = userCode;
+ deviceCodeSection.classList.toggle('hidden', !userCode);
+ deviceCodeSection.style.display = userCode ? 'block' : 'none';
}
-export async function validateToken(token, toastManager, options = {}) {
+function setStatus(message = '', statusClass = '') {
const statusEl = document.getElementById('tokenStatus');
- const shouldApplyResult = options.shouldApplyResult ?? (() => true);
- try {
- const response = await fetch('https://api.github.com/user', {
- headers: createHeaders(token)
- });
+ if (!statusEl) {
+ return;
+ }
- if (response.ok) {
- const user = await response.json();
- if (!shouldApplyResult()) {
- return { isValid: true, user: user.login };
- }
+ statusEl.textContent = message;
+ statusEl.className = `token-status${statusClass ? ` ${statusClass}` : ''}`;
+}
- statusEl.textContent = `✓ Valid (${user.login})`;
- statusEl.className = 'token-status valid';
- document.getElementById('clearTokenBtn').style.display = 'block';
-
- const repoInput = document.getElementById('repoInput');
- repoInput.disabled = false;
- repoInput.placeholder = 'e.g., react, facebook/react, or GitHub URL';
- document.getElementById('addRepoBtn').disabled = false;
- document.getElementById('repoHelpText').textContent = 'Add repositories to monitor (npm package, owner/repo, or GitHub URL)';
-
- const importSection = document.getElementById('importReposSection');
- importSection.classList.remove('hidden');
- importSection.style.display = 'block';
-
- if (!toastManager.lastValidToken || toastManager.lastValidToken !== token) {
- if (toastManager.isManualTokenEntry) {
- notifications.success(`GitHub token validated successfully for user: ${user.login}`);
- }
- toastManager.lastValidToken = token;
- }
+export function applyStoredConnection(authSession, options = {}) {
+ const connectBtn = document.getElementById('connectGitHubBtn');
+ const clearBtn = document.getElementById('clearTokenBtn');
+ const isConnected = Boolean(authSession?.accessToken);
+ const username = authSession?.username;
- toastManager.isManualTokenEntry = false;
- return { isValid: true, user: user.login };
- } else if (response.status === 401) {
- if (!shouldApplyResult()) {
- return { isValid: false, reason: 'invalid' };
- }
+ setRepoAccessState(isConnected);
+ setDeviceCode(options.userCode || '');
- statusEl.textContent = '✗ Invalid token';
- statusEl.className = 'token-status invalid';
- document.getElementById('clearTokenBtn').style.display = 'none';
+ if (connectBtn) {
+ connectBtn.disabled = false;
+ connectBtn.textContent = isConnected ? 'Reconnect GitHub' : 'Connect GitHub';
+ }
- const repoInput = document.getElementById('repoInput');
- repoInput.disabled = true;
- repoInput.placeholder = 'Enter a valid GitHub token to add repositories';
- document.getElementById('addRepoBtn').disabled = true;
- document.getElementById('repoHelpText').textContent = 'Invalid token. Please check your GitHub token and try again.';
+ if (clearBtn) {
+ clearBtn.style.display = isConnected ? 'block' : 'none';
+ }
- const importSection = document.getElementById('importReposSection');
- importSection.classList.add('hidden');
- importSection.style.display = 'none';
+ if (options.statusMessage) {
+ setStatus(options.statusMessage, options.statusClass);
+ return;
+ }
- if (!toastManager.lastInvalidToken || toastManager.lastInvalidToken !== token) {
- notifications.error('Invalid GitHub token. Please check your token and try again.');
- toastManager.lastInvalidToken = token;
- }
- return { isValid: false, reason: 'invalid' };
- } else {
- if (!shouldApplyResult()) {
- return { isValid: false, reason: 'http', status: response.status };
- }
+ if (isConnected) {
+ setStatus(
+ username ? `Connected as ${username}` : 'GitHub is connected',
+ 'valid'
+ );
+ } else {
+ setStatus('', '');
+ }
+}
- statusEl.textContent = `✗ Error (${response.status})`;
- statusEl.className = 'token-status invalid';
- document.getElementById('clearTokenBtn').style.display = 'none';
+export async function clearToken() {
+ if (!confirm('Disconnect GitHub from DevWatch?')) {
+ return false;
+ }
+
+ await clearAuthSession();
+
+ applyStoredConnection(null);
+ notifications.info('GitHub disconnected');
+ return true;
+}
+
+function getErrorMessage(error) {
+ switch (error?.code) {
+ case 'client_id_missing':
+ return 'GitHub OAuth client ID is not configured for this build yet.';
+ case 'access_denied':
+ return 'GitHub sign-in was cancelled before access was granted.';
+ case 'expired_token':
+ return 'The GitHub sign-in code expired. Start again to reconnect.';
+ case 'aborted':
+ return 'GitHub sign-in was cancelled.';
+ default:
+ return 'GitHub sign-in failed. Try again in a moment.';
+ }
+}
+
+export async function connectGitHub(_toastManager) {
+ const previousSession = await getAuthSession();
+ const connectBtn = document.getElementById('connectGitHubBtn');
+ let nextButtonLabel = previousSession?.accessToken ? 'Reconnect GitHub' : 'Connect GitHub';
- const repoInput = document.getElementById('repoInput');
- repoInput.disabled = true;
- repoInput.placeholder = 'Enter a valid GitHub token to add repositories';
- document.getElementById('addRepoBtn').disabled = true;
- document.getElementById('repoHelpText').textContent = 'GitHub API error. Please try again later.';
+ if (connectBtn) {
+ connectBtn.disabled = true;
+ connectBtn.textContent = 'Waiting for GitHub...';
+ }
- const importSection = document.getElementById('importReposSection');
- importSection.classList.add('hidden');
- importSection.style.display = 'none';
+ setRepoAccessState(Boolean(previousSession?.accessToken));
+ setStatus('Starting GitHub sign-in...', 'checking');
- if (!toastManager.lastApiError || toastManager.lastApiError !== response.status) {
- notifications.error(`GitHub API error (${response.status}). Please try again later.`);
- toastManager.lastApiError = response.status;
+ try {
+ const result = await completeGitHubDeviceAuth({
+ onCode: ({ userCode }) => {
+ setDeviceCode(userCode || '');
+ setStatus(`Enter ${userCode} on GitHub to finish connecting.`, 'checking');
}
- return { isValid: false, reason: 'http', status: response.status };
- }
- } catch (_error) {
- if (!shouldApplyResult()) {
- return { isValid: false, reason: 'network' };
- }
+ });
- statusEl.textContent = '✗ Network error';
- statusEl.className = 'token-status invalid';
- document.getElementById('clearTokenBtn').style.display = 'none';
+ await setAuthSession(result.authSession);
- const repoInput = document.getElementById('repoInput');
- repoInput.disabled = true;
- repoInput.placeholder = 'Enter a valid GitHub token to add repositories';
- document.getElementById('addRepoBtn').disabled = true;
- document.getElementById('repoHelpText').textContent = 'Network error. Please check your connection and try again.';
+ applyStoredConnection(result.authSession);
+ nextButtonLabel = 'Reconnect GitHub';
+ notifications.success(`Connected to GitHub as ${result.user.login}`);
+ return { isValid: true, user: result.user.login, authSession: result.authSession };
+ } catch (error) {
+ applyStoredConnection(previousSession, {
+ statusMessage: getErrorMessage(error),
+ statusClass: 'invalid'
+ });
- document.getElementById('importReposSection').style.display = 'none';
+ if (previousSession?.accessToken) {
+ notifications.warning(getErrorMessage(error));
+ } else {
+ notifications.error(getErrorMessage(error));
+ }
- notifications.error('Network error while validating token. Please check your connection and try again.');
- return { isValid: false, reason: 'network' };
+ return { isValid: false, reason: error?.code || 'auth_failed' };
+ } finally {
+ if (connectBtn) {
+ connectBtn.disabled = false;
+ connectBtn.textContent = nextButtonLabel;
+ }
}
}
+
+export function getDisconnectHelpUrl() {
+ return OAUTH_CONFIG.AUTHORIZED_APPS_URL;
+}
diff --git a/options/options.css b/options/options.css
index 7d20bb8..e5b35a6 100644
--- a/options/options.css
+++ b/options/options.css
@@ -2199,6 +2199,27 @@ body.dark-mode .notification-toggle input:checked + .toggle-slider {
flex: 1;
}
+.auth-button-row {
+ flex-wrap: wrap;
+}
+
+#deviceCodeSection {
+ flex-direction: column;
+ align-items: stretch;
+}
+
+.device-code-label {
+ font-size: 12px;
+ font-weight: 600;
+ color: var(--text-secondary);
+}
+
+#deviceCodeSection input {
+ letter-spacing: 0.12em;
+ font-weight: 600;
+ text-align: center;
+}
+
.token-status {
display: block;
font-size: 12px;
diff --git a/options/options.html b/options/options.html
index d0704b0..e6471d8 100644
--- a/options/options.html
+++ b/options/options.html
@@ -65,44 +65,47 @@
Getting Started
1
-
-
Create a GitHub Token
-
This allows the extension to fetch your repository activity securely.
-
- Security: Your token is encrypted with AES-GCM encryption and stored securely on your device. It's never transmitted to third parties.
-
-
-
-
+
+
Connect GitHub
+
Sign in once so DevWatch can check the repositories you want to follow.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Click Connect GitHub, approve access on GitHub, and come back here once the browser tab says you can close it.
+
+
+
+
+
- Expiration: Tokens expire after 90 days by default. You'll need to create a new token when yours expires, or select "No expiration" when creating your token (not recommended for security).
-
-
-
+
+
+ Security: Your GitHub sign-in session is encrypted with AES-GCM and stored locally on your device.
+
+
+
+
+
+ Permissions: DevWatch requests repo and read:user access so it can read repository activity and identify your account.
+
+
+
@@ -153,7 +156,7 @@
View Help & Changelog
Add New Repository
-
Add a valid GitHub token above to start adding repositories
+
Connect GitHub above to start adding repositories
@@ -611,7 +614,7 @@
Clear All Data
Reset to Defaults
-
Reset all settings to default values. Your token and repositories will be cleared.
+
Reset all settings to default values. Your GitHub connection and repositories will be cleared.