diff --git a/backend/services/authService.js b/backend/services/authService.js index 51367e5..d0dc197 100644 --- a/backend/services/authService.js +++ b/backend/services/authService.js @@ -19,6 +19,25 @@ const { Octokit } = require('@octokit/rest'); const GOOGLE_NOT_AVAILABLE = 'Google auth is not available until Phase 3'; +/** Current supported REST calendar version — avoids 2022-11-28 deprecation noise. */ +const GITHUB_API_VERSION = '2026-03-10'; + +/** + * Build an Octokit client that always sends X-GitHub-Api-Version. + * Constructor `request.headers` is ignored by @octokit/rest@20 — must use defaults. + * + * @param {object} [options] + * @param {string} [options.auth] + * @returns {import('@octokit/rest').Octokit} + */ +function createGitHubOctokit(options = {}) { + const octokit = new Octokit(options); + octokit.request = octokit.request.defaults({ + headers: { 'X-GitHub-Api-Version': GITHUB_API_VERSION }, + }); + return octokit; +} + class AuthService { /** * @param {object} [deps] @@ -116,7 +135,7 @@ class AuthService { if (this._hasGitHubAppSigningCredentials()) { try { - const appOctokit = new Octokit({ auth: this._createAppJwt() }); + const appOctokit = createGitHubOctokit({ auth: this._createAppJwt() }); const { data } = await appOctokit.rest.apps.getRepoInstallation({ owner, repo, @@ -160,7 +179,7 @@ class AuthService { if (!this._hasGitHubAppSigningCredentials()) { return installUrl; } - const appOctokit = new Octokit({ auth: this._createAppJwt() }); + const appOctokit = createGitHubOctokit({ auth: this._createAppJwt() }); const { data } = await appOctokit.rest.apps.getAuthenticated(); if (data?.slug) { installUrl = `https://github.com/apps/${data.slug}/installations/new`; @@ -249,12 +268,12 @@ class AuthService { throw new Error(await this._installationSetupMessage(fullName)); } - const appOctokit = new Octokit({ auth: this._createAppJwt() }); + const appOctokit = createGitHubOctokit({ auth: this._createAppJwt() }); const { data } = await appOctokit.rest.apps.createInstallationAccessToken({ installation_id: installationId, }); - return new Octokit({ auth: data.token }); + return createGitHubOctokit({ auth: data.token }); } /** @param {string | undefined} b64 */ @@ -437,7 +456,7 @@ class AuthService { } const accessToken = this.decrypt(encrypted); - return new Octokit({ auth: accessToken }); + return createGitHubOctokit({ auth: accessToken }); } /** @@ -538,3 +557,5 @@ class AuthService { } module.exports = AuthService; +module.exports.createGitHubOctokit = createGitHubOctokit; +module.exports.GITHUB_API_VERSION = GITHUB_API_VERSION; diff --git a/backend/tests/authService.test.js b/backend/tests/authService.test.js index 09e484e..fc61690 100644 --- a/backend/tests/authService.test.js +++ b/backend/tests/authService.test.js @@ -94,6 +94,35 @@ test('getGitHubClient returns authenticated Octokit', () => { assert.equal(typeof client.rest.repos.listForAuthenticatedUser, 'function'); }); +test('createGitHubOctokit sends X-GitHub-Api-Version on requests', async () => { + const { createGitHubOctokit, GITHUB_API_VERSION } = require('../services/authService'); + /** @type {Headers | Record | undefined} */ + let capturedHeaders; + + const client = createGitHubOctokit({ + auth: 'gho_test_token', + request: { + fetch: async (_url, options = {}) => { + capturedHeaders = options.headers; + return new Response(JSON.stringify({ login: 'sam' }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }); + }, + }, + }); + + await client.rest.users.getAuthenticated(); + + assert.ok(capturedHeaders); + const headers = + typeof capturedHeaders.get === 'function' + ? capturedHeaders + : new Headers(capturedHeaders); + assert.equal(headers.get('X-GitHub-Api-Version'), GITHUB_API_VERSION); + assert.equal(GITHUB_API_VERSION, '2026-03-10'); +}); + test('getGoogleDriveClient returns null until Phase 3', () => { const authService = new AuthService({ sessionSecret: TEST_SESSION_SECRET,