@symbiosis-lab/moss-plugin-github 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +18 -0
  3. package/assets/icon.svg +3 -0
  4. package/assets/manifest.json +19 -0
  5. package/e2e/deploy-api.test.ts +1129 -0
  6. package/e2e/moss-cli.test.ts +478 -0
  7. package/features/auth/device-flow.feature +41 -0
  8. package/features/deploy/validation.feature +50 -0
  9. package/features/steps/auth.steps.ts +285 -0
  10. package/features/steps/deploy.steps.ts +354 -0
  11. package/package.json +51 -0
  12. package/src/__tests__/auth-flow.integration.test.ts +738 -0
  13. package/src/__tests__/auth.test.ts +147 -0
  14. package/src/__tests__/configure-domain.test.ts +263 -0
  15. package/src/__tests__/deploy.integration.test.ts +798 -0
  16. package/src/__tests__/git.test.ts +190 -0
  17. package/src/__tests__/github-api.test.ts +761 -0
  18. package/src/__tests__/github-deploy.test.ts +2411 -0
  19. package/src/__tests__/progress-timeout.test.ts +209 -0
  20. package/src/__tests__/repo-setup-progress.test.ts +367 -0
  21. package/src/__tests__/repo-setup.test.ts +370 -0
  22. package/src/__tests__/token.test.ts +152 -0
  23. package/src/__tests__/utils.test.ts +129 -0
  24. package/src/__tests__/workflow.test.ts +146 -0
  25. package/src/auth.ts +588 -0
  26. package/src/constants.ts +7 -0
  27. package/src/git.ts +60 -0
  28. package/src/github-api.ts +601 -0
  29. package/src/github-deploy.ts +593 -0
  30. package/src/main.ts +646 -0
  31. package/src/repo-setup.ts +685 -0
  32. package/src/token.ts +202 -0
  33. package/src/types.ts +91 -0
  34. package/src/utils.ts +108 -0
  35. package/src/workflow.ts +79 -0
  36. package/test-helpers/mock-github-api.ts +217 -0
  37. package/tsconfig.json +20 -0
  38. package/vitest.config.ts +50 -0
package/src/token.ts ADDED
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Token Storage Module
3
+ *
4
+ * Handles secure storage and retrieval of GitHub access tokens.
5
+ *
6
+ * Storage strategy:
7
+ * 1. Primary: Plugin cookies (via moss-api getPluginCookie/setPluginCookie)
8
+ * 2. Fallback: In-memory cache (for current session)
9
+ *
10
+ * Tokens are used for GitHub REST API authentication.
11
+ * The git credential helper is checked opportunistically (works when git
12
+ * is installed, silently falls through to OAuth when it's not).
13
+ */
14
+
15
+ import { getPluginCookie, setPluginCookie, executeBinary } from "@symbiosis-lab/moss-api";
16
+
17
+ const GITHUB_HOST = "github.com";
18
+ const TOKEN_COOKIE_NAME = "__github_access_token";
19
+
20
+ // In-memory fallback cache
21
+ let cachedToken: string | null = null;
22
+
23
+ /**
24
+ * Format credentials for git credential helper input
25
+ * (Used for documentation and potential future stdin support)
26
+ */
27
+ export function formatCredentialInput(
28
+ host: string,
29
+ protocol: string,
30
+ username?: string,
31
+ password?: string
32
+ ): string {
33
+ const lines = [`protocol=${protocol}`, `host=${host}`];
34
+ if (username) lines.push(`username=${username}`);
35
+ if (password) lines.push(`password=${password}`);
36
+ lines.push(""); // Empty line to signal end of input
37
+ return lines.join("\n");
38
+ }
39
+
40
+ /**
41
+ * Parse git credential helper output
42
+ */
43
+ export function parseCredentialOutput(output: string): {
44
+ username?: string;
45
+ password?: string;
46
+ } {
47
+ const result: { username?: string; password?: string } = {};
48
+
49
+ for (const line of output.split("\n")) {
50
+ const [key, ...valueParts] = line.split("=");
51
+ const value = valueParts.join("="); // Handle = in values
52
+
53
+ if (key === "username") {
54
+ result.username = value;
55
+ } else if (key === "password") {
56
+ result.password = value;
57
+ }
58
+ }
59
+
60
+ return result;
61
+ }
62
+
63
+ /**
64
+ * Try to retrieve GitHub token from git credential helper
65
+ *
66
+ * Uses `git credential fill` with stdin input to query the system's
67
+ * configured credential helper (e.g., macOS Keychain, Windows Credential Manager).
68
+ *
69
+ * @returns The token if found in git credentials, null otherwise
70
+ */
71
+ export async function getTokenFromGit(gitPath: string = "git"): Promise<string | null> {
72
+ try {
73
+ console.log(" Checking git credential helper for GitHub token...");
74
+
75
+ // Format the credential request for github.com
76
+ const input = formatCredentialInput(GITHUB_HOST, "https");
77
+
78
+ // Execute git credential fill with stdin
79
+ const result = await executeBinary({
80
+ binaryPath: gitPath,
81
+ args: ["credential", "fill"],
82
+ stdin: input,
83
+ timeoutMs: 5000,
84
+ });
85
+
86
+ if (!result.success) {
87
+ console.log(" No credentials found in git credential helper");
88
+ return null;
89
+ }
90
+
91
+ // Parse the credential output
92
+ const { password } = parseCredentialOutput(result.stdout);
93
+
94
+ if (password) {
95
+ console.log(" Found GitHub token in git credential helper");
96
+ return password;
97
+ }
98
+
99
+ console.log(" Git credential helper returned no password");
100
+ return null;
101
+ } catch (error) {
102
+ console.log(` Git credential helper failed: ${error}`);
103
+ return null;
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Store a GitHub access token
109
+ *
110
+ * Uses plugin cookie storage with in-memory fallback.
111
+ * Note: Plugin identity and project path are auto-detected from runtime context.
112
+ */
113
+ export async function storeToken(token: string): Promise<boolean> {
114
+ try {
115
+ console.log(" Storing GitHub access token...");
116
+
117
+ // Store in plugin cookies
118
+ try {
119
+ await setPluginCookie([
120
+ {
121
+ name: TOKEN_COOKIE_NAME,
122
+ value: token,
123
+ domain: GITHUB_HOST,
124
+ },
125
+ ]);
126
+ console.log(" Token stored in plugin cookies");
127
+ } catch (error) {
128
+ console.warn(` Could not store in cookies: ${error}`);
129
+ }
130
+
131
+ // Always cache in memory as fallback
132
+ cachedToken = token;
133
+
134
+ console.log(" Token stored successfully");
135
+ return true;
136
+ } catch (error) {
137
+ console.error(` Error storing token: ${error}`);
138
+ return false;
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Retrieve GitHub access token
144
+ *
145
+ * Checks plugin cookies first, then falls back to memory cache.
146
+ * Note: Plugin identity and project path are auto-detected from runtime context.
147
+ */
148
+ export async function getToken(): Promise<string | null> {
149
+ // Check memory cache first (faster)
150
+ if (cachedToken) {
151
+ return cachedToken;
152
+ }
153
+
154
+ // Try plugin cookies
155
+ try {
156
+ const cookies = await getPluginCookie();
157
+ const tokenCookie = cookies?.find((c) => c.name === TOKEN_COOKIE_NAME);
158
+
159
+ if (tokenCookie) {
160
+ cachedToken = tokenCookie.value;
161
+ return cachedToken;
162
+ }
163
+ } catch {
164
+ // Cookie retrieval failed, token not available
165
+ }
166
+
167
+ return null;
168
+ }
169
+
170
+ /**
171
+ * Clear the cached token
172
+ */
173
+ export function clearTokenCache(): void {
174
+ cachedToken = null;
175
+ }
176
+
177
+ /**
178
+ * Remove GitHub access token
179
+ * Note: Plugin identity and project path are auto-detected from runtime context.
180
+ */
181
+ export async function clearToken(): Promise<boolean> {
182
+ try {
183
+ console.log(" Clearing GitHub access token...");
184
+
185
+ // Clear from plugin cookies
186
+ try {
187
+ await setPluginCookie([]);
188
+ } catch {
189
+ // Ignore cookie clear errors
190
+ }
191
+
192
+ // Clear memory cache
193
+ cachedToken = null;
194
+
195
+ console.log(" Token cleared successfully");
196
+ return true;
197
+ } catch (error) {
198
+ console.error(` Error clearing token: ${error}`);
199
+ return false;
200
+ }
201
+ }
202
+
package/src/types.ts ADDED
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Plugin-specific type definitions for the GitHub Pages Deployer Plugin
3
+ *
4
+ * Common types (DeployContext, HookResult, PluginMessage, etc.) are imported
5
+ * from @symbiosis-lab/moss-api.
6
+ */
7
+
8
+ // Re-export SDK types for convenience
9
+ export type {
10
+ DeployContext,
11
+ ConfigureDomainContext,
12
+ HookResult,
13
+ DeploymentInfo,
14
+ ProjectInfo,
15
+ PluginMessage,
16
+ DnsTarget,
17
+ DnsRecord,
18
+ } from "@symbiosis-lab/moss-api";
19
+
20
+ // ============================================================================
21
+ // GitHub OAuth Device Flow Types
22
+ // ============================================================================
23
+
24
+ /**
25
+ * Response from GitHub's device code endpoint
26
+ * POST https://github.com/login/device/code
27
+ */
28
+ export interface DeviceCodeResponse {
29
+ /** The device verification code (used for polling) */
30
+ device_code: string;
31
+ /** The user code to display to the user */
32
+ user_code: string;
33
+ /** The URL where user enters the code */
34
+ verification_uri: string;
35
+ /** Pre-filled URL with user_code embedded (user just clicks "Continue") */
36
+ verification_uri_complete?: string;
37
+ /** Seconds until the codes expire */
38
+ expires_in: number;
39
+ /** Minimum seconds between poll requests */
40
+ interval: number;
41
+ }
42
+
43
+ /**
44
+ * Response from GitHub's access token endpoint
45
+ * POST https://github.com/login/oauth/access_token
46
+ */
47
+ export interface TokenResponse {
48
+ /** The access token (present on success) */
49
+ access_token?: string;
50
+ /** Token type, usually "bearer" */
51
+ token_type?: string;
52
+ /** Granted scopes */
53
+ scope?: string;
54
+ /** Error code (present on failure) */
55
+ error?: string;
56
+ /** Human-readable error description */
57
+ error_description?: string;
58
+ }
59
+
60
+ /**
61
+ * GitHub user response from /user endpoint
62
+ * Used to validate tokens
63
+ */
64
+ export interface GitHubUser {
65
+ login: string;
66
+ id: number;
67
+ name?: string;
68
+ email?: string;
69
+ }
70
+
71
+ /**
72
+ * Authentication state for the plugin
73
+ */
74
+ export interface AuthState {
75
+ /** Whether user is authenticated */
76
+ isAuthenticated: boolean;
77
+ /** GitHub username if authenticated */
78
+ username?: string;
79
+ /** Token scopes if authenticated */
80
+ scopes?: string[];
81
+ }
82
+
83
+ /**
84
+ * Configuration for the auth module
85
+ */
86
+ export interface AuthConfig {
87
+ /** GitHub OAuth App Client ID */
88
+ clientId: string;
89
+ /** Required OAuth scopes */
90
+ scopes: string[];
91
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Utility functions for the GitHub Deployer Plugin
3
+ *
4
+ * This module wraps SDK utilities with plugin-specific functionality.
5
+ */
6
+
7
+ import {
8
+ setMessageContext,
9
+ reportProgress as sdkReportProgress,
10
+ reportError as sdkReportError,
11
+ showToast as sdkShowToast,
12
+ dismissToast as sdkDismissToast,
13
+ closeBrowser as sdkCloseBrowser,
14
+ type ToastOptions,
15
+ } from "@symbiosis-lab/moss-api";
16
+
17
+ // Re-export ToastOptions type for convenience
18
+ export type { ToastOptions };
19
+
20
+ // ============================================================================
21
+ // Plugin Configuration
22
+ // ============================================================================
23
+
24
+ const PLUGIN_NAME = "github";
25
+
26
+ // Initialize message context on load
27
+ setMessageContext(PLUGIN_NAME, "deploy");
28
+
29
+ // ============================================================================
30
+ // Re-exports from SDK (with plugin context)
31
+ // ============================================================================
32
+
33
+ /**
34
+ * Set the current hook name for message routing
35
+ */
36
+ export function setCurrentHookName(name: string): void {
37
+ setMessageContext(PLUGIN_NAME, name);
38
+ }
39
+
40
+ /**
41
+ * Report progress to moss during long-running operations
42
+ */
43
+ export async function reportProgress(
44
+ phase: string,
45
+ current: number,
46
+ total: number,
47
+ message?: string
48
+ ): Promise<void> {
49
+ await sdkReportProgress(phase, current, total, message);
50
+ }
51
+
52
+ /**
53
+ * Report an error to moss during hook execution
54
+ */
55
+ export async function reportError(
56
+ error: string,
57
+ context?: string,
58
+ fatal = false
59
+ ): Promise<void> {
60
+ await sdkReportError(error, context, fatal);
61
+ }
62
+
63
+ // ============================================================================
64
+ // Toast Utilities
65
+ // ============================================================================
66
+
67
+ /**
68
+ * Show a toast notification in the main moss UI
69
+ *
70
+ * @example
71
+ * ```typescript
72
+ * // Success toast with clickable action
73
+ * await showToast({
74
+ * message: "Deployed!",
75
+ * variant: "success",
76
+ * actions: [{ label: "View site", url: "https://..." }],
77
+ * duration: 8000
78
+ * });
79
+ * ```
80
+ */
81
+ export async function showToast(options: ToastOptions | string): Promise<void> {
82
+ await sdkShowToast(options);
83
+ }
84
+
85
+ /**
86
+ * Dismiss a toast by ID
87
+ */
88
+ export async function dismissToast(id: string): Promise<void> {
89
+ await sdkDismissToast(id);
90
+ }
91
+
92
+ /**
93
+ * Close the action panel
94
+ */
95
+ export async function closeBrowser(): Promise<void> {
96
+ await sdkCloseBrowser();
97
+ }
98
+
99
+ // ============================================================================
100
+ // Async Utilities
101
+ // ============================================================================
102
+
103
+ /**
104
+ * Sleep for specified milliseconds
105
+ */
106
+ export function sleep(ms: number): Promise<void> {
107
+ return new Promise((resolve) => setTimeout(resolve, ms));
108
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * GitHub Actions workflow template and creation
3
+ */
4
+
5
+ import { writeFile, fileExists } from "@symbiosis-lab/moss-api";
6
+
7
+ /**
8
+ * GitHub Actions workflow template for deploying to GitHub Pages
9
+ * This is extracted from the original deploy.rs
10
+ */
11
+ export const WORKFLOW_TEMPLATE = `# moss GitHub Pages Deployment
12
+ # This workflow deploys your pre-built site from .moss/build/current/ to GitHub Pages
13
+ # Generated by moss - https://mosspub.com
14
+
15
+ name: Deploy to GitHub Pages
16
+
17
+ on:
18
+ push:
19
+ branches: [BRANCH_PLACEHOLDER]
20
+ workflow_dispatch:
21
+
22
+ permissions:
23
+ contents: read
24
+ pages: write
25
+ id-token: write
26
+
27
+ concurrency:
28
+ group: "pages"
29
+ cancel-in-progress: false
30
+
31
+ jobs:
32
+ deploy:
33
+ runs-on: ubuntu-latest
34
+ environment:
35
+ name: github-pages
36
+ url: \${{ steps.deployment.outputs.page_url }}
37
+ steps:
38
+ - name: Checkout
39
+ uses: actions/checkout@v4
40
+
41
+ - name: Setup Pages
42
+ uses: actions/configure-pages@v4
43
+
44
+ - name: Upload artifact
45
+ uses: actions/upload-pages-artifact@v3
46
+ with:
47
+ path: .moss/build/current
48
+
49
+ - name: Deploy to GitHub Pages
50
+ id: deployment
51
+ uses: actions/deploy-pages@v4
52
+ `;
53
+
54
+ /**
55
+ * Generate the workflow content with the correct branch
56
+ */
57
+ export function generateWorkflowContent(branch: string): string {
58
+ return WORKFLOW_TEMPLATE.replace("BRANCH_PLACEHOLDER", branch);
59
+ }
60
+
61
+ /**
62
+ * Create the GitHub Actions workflow file
63
+ */
64
+ export async function createWorkflowFile(branch: string): Promise<void> {
65
+ console.log(" Creating .github/workflows/moss-deploy.yml...");
66
+
67
+ const content = generateWorkflowContent(branch);
68
+
69
+ await writeFile(".github/workflows/moss-deploy.yml", content);
70
+
71
+ console.log(" Workflow file created");
72
+ }
73
+
74
+ /**
75
+ * Check if the workflow file already exists
76
+ */
77
+ export async function workflowExists(): Promise<boolean> {
78
+ return fileExists(".github/workflows/moss-deploy.yml");
79
+ }
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Mock GitHub API helpers for integration testing
3
+ *
4
+ * Provides utilities to mock GitHub OAuth Device Flow responses
5
+ * for testing authentication without hitting real APIs.
6
+ */
7
+
8
+ import type { DeviceCodeResponse, TokenResponse, GitHubUser } from "../src/types";
9
+
10
+ /**
11
+ * Configuration for mocking GitHub API responses
12
+ */
13
+ export interface MockGitHubConfig {
14
+ /** Response for POST /login/device/code */
15
+ deviceCodeResponse?: DeviceCodeResponse;
16
+ /** Response(s) for POST /login/oauth/access_token - can be a single response or array for sequence */
17
+ tokenResponse?: TokenResponse | TokenResponse[];
18
+ /** Response for GET /user (token validation) */
19
+ userResponse?: GitHubUser | null;
20
+ /** OAuth scopes to return in X-OAuth-Scopes header */
21
+ scopes?: string[];
22
+ }
23
+
24
+ /**
25
+ * Default mock responses for successful auth flow
26
+ */
27
+ export const defaultDeviceCodeResponse: DeviceCodeResponse = {
28
+ device_code: "test-device-code-123",
29
+ user_code: "ABCD-1234",
30
+ verification_uri: "https://github.com/login/device",
31
+ verification_uri_complete: "https://github.com/login/device?user_code=ABCD-1234",
32
+ expires_in: 900,
33
+ interval: 5,
34
+ };
35
+
36
+ export const defaultTokenResponse: TokenResponse = {
37
+ access_token: "gho_test_token_abc123",
38
+ token_type: "bearer",
39
+ scope: "repo,workflow",
40
+ };
41
+
42
+ export const defaultUserResponse: GitHubUser = {
43
+ login: "testuser",
44
+ id: 12345,
45
+ name: "Test User",
46
+ email: "test@example.com",
47
+ };
48
+
49
+ /**
50
+ * Error responses for various failure scenarios
51
+ */
52
+ export const authorizationPendingResponse: TokenResponse = {
53
+ error: "authorization_pending",
54
+ error_description: "The authorization request is still pending.",
55
+ };
56
+
57
+ export const expiredTokenResponse: TokenResponse = {
58
+ error: "expired_token",
59
+ error_description: "The device_code has expired.",
60
+ };
61
+
62
+ export const accessDeniedResponse: TokenResponse = {
63
+ error: "access_denied",
64
+ error_description: "The user denied authorization.",
65
+ };
66
+
67
+ export const slowDownResponse: TokenResponse = {
68
+ error: "slow_down",
69
+ error_description: "Too many requests. Please slow down.",
70
+ };
71
+
72
+ /**
73
+ * Create a mock Response object
74
+ */
75
+ function createMockResponse(body: unknown, status = 200, headers: Record<string, string> = {}): Response {
76
+ return {
77
+ ok: status >= 200 && status < 300,
78
+ status,
79
+ statusText: status === 200 ? "OK" : "Error",
80
+ headers: new Headers(headers),
81
+ json: async () => body,
82
+ text: async () => JSON.stringify(body),
83
+ } as Response;
84
+ }
85
+
86
+ /**
87
+ * Create a mock fetch function for testing
88
+ */
89
+ export function createMockFetch(config: MockGitHubConfig = {}) {
90
+ let tokenResponseIndex = 0;
91
+
92
+ return async (url: string | URL | Request, _init?: RequestInit): Promise<Response> => {
93
+ const urlString = typeof url === "string" ? url : url instanceof URL ? url.toString() : url.url;
94
+
95
+ // Device code endpoint
96
+ if (urlString.includes("/login/device/code")) {
97
+ const response = config.deviceCodeResponse || defaultDeviceCodeResponse;
98
+ return createMockResponse(response);
99
+ }
100
+
101
+ // Token endpoint
102
+ if (urlString.includes("/login/oauth/access_token")) {
103
+ let response: TokenResponse;
104
+
105
+ if (Array.isArray(config.tokenResponse)) {
106
+ // Return responses in sequence
107
+ response = config.tokenResponse[tokenResponseIndex] || config.tokenResponse[config.tokenResponse.length - 1];
108
+ tokenResponseIndex++;
109
+ } else {
110
+ response = config.tokenResponse || defaultTokenResponse;
111
+ }
112
+
113
+ return createMockResponse(response);
114
+ }
115
+
116
+ // User endpoint (token validation)
117
+ if (urlString.includes("/user")) {
118
+ if (config.userResponse === null) {
119
+ return createMockResponse({ message: "Bad credentials" }, 401);
120
+ }
121
+
122
+ const user = config.userResponse || defaultUserResponse;
123
+ const scopes = config.scopes || ["repo", "workflow"];
124
+
125
+ return createMockResponse(user, 200, {
126
+ "X-OAuth-Scopes": scopes.join(", "),
127
+ });
128
+ }
129
+
130
+ // Unknown endpoint
131
+ throw new Error(`Unmocked URL: ${urlString}`);
132
+ };
133
+ }
134
+
135
+ /**
136
+ * Mock browser functions for testing
137
+ */
138
+ export function createMockBrowser() {
139
+ let isOpen = false;
140
+ let currentUrl = "";
141
+
142
+ return {
143
+ openBrowser: async (url: string) => {
144
+ isOpen = true;
145
+ currentUrl = url;
146
+ },
147
+ closeBrowser: async () => {
148
+ isOpen = false;
149
+ currentUrl = "";
150
+ },
151
+ isOpen: () => isOpen,
152
+ getCurrentUrl: () => currentUrl,
153
+ };
154
+ }
155
+
156
+ /**
157
+ * Mock git credential helper for testing
158
+ */
159
+ export function createMockCredentialHelper() {
160
+ const store: Map<string, { username: string; password: string }> = new Map();
161
+
162
+ return {
163
+ store: (host: string, username: string, password: string) => {
164
+ store.set(host, { username, password });
165
+ },
166
+ get: (host: string) => store.get(host),
167
+ clear: (host: string) => store.delete(host),
168
+ hasCredentials: (host: string) => store.has(host),
169
+ reset: () => store.clear(),
170
+ };
171
+ }
172
+
173
+ /**
174
+ * Setup GitHub API mocks using MockTauriContext.urlConfig
175
+ *
176
+ * This sets up URL responses for the GitHub OAuth Device Flow endpoints
177
+ * to work with moss-api's httpPost function which uses Tauri IPC.
178
+ *
179
+ * @param ctx - MockTauriContext from setupMockTauri()
180
+ * @param config - Mock response configuration
181
+ */
182
+ export function setupGitHubApiMocks(
183
+ ctx: { urlConfig: { setResponse: (url: string, response: Record<string, unknown>) => void } },
184
+ config: MockGitHubConfig = {}
185
+ ): void {
186
+ // Device code endpoint
187
+ const deviceCodeResponse = config.deviceCodeResponse || defaultDeviceCodeResponse;
188
+ ctx.urlConfig.setResponse("https://github.com/login/device/code", {
189
+ status: 200,
190
+ ok: true,
191
+ contentType: "application/json",
192
+ bodyBase64: btoa(JSON.stringify(deviceCodeResponse)),
193
+ });
194
+
195
+ // Token endpoint - handle single response or first in sequence
196
+ const tokenResponse = Array.isArray(config.tokenResponse)
197
+ ? config.tokenResponse[0]
198
+ : config.tokenResponse || defaultTokenResponse;
199
+ ctx.urlConfig.setResponse("https://github.com/login/oauth/access_token", {
200
+ status: 200,
201
+ ok: true,
202
+ contentType: "application/json",
203
+ bodyBase64: btoa(JSON.stringify(tokenResponse)),
204
+ });
205
+
206
+ // User endpoint (for token validation - GET request, uses fetch_url)
207
+ const userResponse = config.userResponse !== null
208
+ ? config.userResponse || defaultUserResponse
209
+ : { message: "Bad credentials" };
210
+ const userStatus = config.userResponse === null ? 401 : 200;
211
+ ctx.urlConfig.setResponse("https://api.github.com/user", {
212
+ status: userStatus,
213
+ ok: userStatus === 200,
214
+ contentType: "application/json",
215
+ bodyBase64: btoa(JSON.stringify(userResponse)),
216
+ });
217
+ }