All files / src/entities/context profile.ts

100% Statements 33/33
100% Branches 22/22
100% Functions 4/4
100% Lines 32/32

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106                  231x 231x 231x 231x 231x 231x                     231x                                         231x 115x     115x       9x 9x 7x 2x 2x   5x   2x     2x         8x 4x 4x   1x                   231x 9x     9x 9x   9x 9x 9x 1x     8x 8x              
/**
 * Account profile of the connection
 *
 * The identity comes from the validated credentials of the request: the GitLab instance and
 * the user GitLab reports for the token. The id is opaque and stable across token refresh
 * and reconnect, and differs for equal user ids on different instances; display values are
 * returned only when GitLab provides them.
 */
 
import { createHash } from 'node:crypto';
import { logDebug } from '../../logger';
import { getGitLabApiUrlFromContext, getTokenContext } from '../../oauth/token-context';
import { enhancedFetch } from '../../utils/fetch';
import { getGitLabBaseUrl } from '../../utils/gitlab-base-url';
import { normalizeInstanceUrl } from '../../utils/url';
 
/** Profile returned to the host (OpenAI `openai/profile` contract). */
export interface AccountProfile {
  id: string;
  name?: string;
  email?: string;
  nickname?: string;
}
 
/** Output schema of the profile tool; the host renders exactly these fields. */
export const ACCOUNT_PROFILE_OUTPUT_SCHEMA = {
  type: 'object' as const,
  properties: {
    id: { type: 'string', minLength: 1, pattern: String.raw`\S` },
    name: { type: 'string' },
    email: { type: 'string' },
    nickname: { type: 'string' },
  },
  required: ['id'],
  additionalProperties: false,
};
 
interface GitLabCurrentUser {
  id: number;
  username: string;
  name?: string;
  email?: string;
  public_email?: string;
}
 
/** Opaque id of one GitLab user on one instance; no name, email or token goes into it. */
export function accountProfileId(instanceUrl: string, gitlabUserId: number): string {
  const digest = createHash('sha256')
    .update(`${normalizeInstanceUrl(instanceUrl)}\n${gitlabUserId}`)
    .digest('base64url');
  return `gitlab:${digest}`;
}
 
async function fetchCurrentUser(instanceUrl: string): Promise<GitLabCurrentUser | null> {
  try {
    const response = await enhancedFetch(`${instanceUrl}/api/v4/user`, { retry: false });
    if (!response.ok) {
      logDebug('Profile: GitLab did not return the current user', { status: response.status });
      return null;
    }
    return (await response.json()) as GitLabCurrentUser;
  } catch (error) {
    logDebug('Profile: failed to fetch the current user', {
      error: error instanceof Error ? error.message : String(error),
    });
    return null;
  }
}
 
function instanceName(instanceUrl: string, label: string | undefined): string {
  if (label) return label;
  try {
    return new URL(instanceUrl).host;
  } catch {
    return instanceUrl;
  }
}
 
/**
 * Resolve the profile of the account behind this request. With OAuth the user id comes
 * from the linked account; otherwise from GitLab for the configured token.
 *
 * @throws Error when no account can be identified
 */
export async function getAccountProfile(): Promise<AccountProfile> {
  const context = getTokenContext();
  // The OAuth account's instance, else the instance of the server's token or the configured
  // instance the chat switched to.
  const instanceUrl = normalizeInstanceUrl(getGitLabApiUrlFromContext() ?? getGitLabBaseUrl());
  const user = await fetchCurrentUser(instanceUrl);
 
  const userId = context?.gitlabUserId ?? user?.id;
  const username = context?.gitlabUsername ?? user?.username;
  if (userId === undefined || username === undefined) {
    throw new Error('GitLab did not identify the account for this connection');
  }
 
  const email = user?.email ?? user?.public_email;
  return {
    id: accountProfileId(instanceUrl, userId),
    ...(user?.name ? { name: user.name } : {}),
    ...(email ? { email } : {}),
    nickname: `${username} @ ${instanceName(instanceUrl, context?.instanceLabel)}`,
  };
}