gitlab-ai-provider 6.9.1 → 6.9.3

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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
4
4
 
5
+ ## <small>6.9.3 (2026-06-11)</small>
6
+
7
+ - Merge branch 'fix/concurrent-auth-token-race' into 'main' ([7306083](https://gitlab.com/vglafirov/gitlab-ai-provider/commit/7306083))
8
+ - fix(auth): prevent concurrent auth failures from token races ([49ed5c7](https://gitlab.com/vglafirov/gitlab-ai-provider/commit/49ed5c7))
9
+
10
+ ## <small>6.9.2 (2026-06-10)</small>
11
+
12
+ - Merge branch 'fix/auth-json-api-token-handling' into 'main' ([19a6e58](https://gitlab.com/vglafirov/gitlab-ai-provider/commit/19a6e58))
13
+ - fix(auth): use PAT and enterpriseUrl-less OAuth tokens from auth.json ([6feeb58](https://gitlab.com/vglafirov/gitlab-ai-provider/commit/6feeb58))
14
+
5
15
  ## <small>6.9.1 (2026-06-10)</small>
6
16
 
7
17
  - Merge branch 'fix-security-review-hardening' into 'main' ([81e627b](https://gitlab.com/vglafirov/gitlab-ai-provider/commit/81e627b))
package/dist/index.d.mts CHANGED
@@ -6,6 +6,11 @@ interface GitLabAnthropicConfig {
6
6
  instanceUrl: string;
7
7
  getHeaders: () => Record<string, string>;
8
8
  fetch?: typeof fetch;
9
+ /**
10
+ * Optional async hook that resolves once credentials are loaded, awaited
11
+ * before the synchronous getHeaders() is used for the direct-access request.
12
+ */
13
+ ensureApiKey?: () => Promise<void>;
9
14
  /**
10
15
  * Optional callback to refresh the API key when a 401 error occurs.
11
16
  * Should clear cached credentials and re-fetch from auth provider.
@@ -1073,6 +1078,7 @@ interface GitLabOpenAIConfig {
1073
1078
  instanceUrl: string;
1074
1079
  getHeaders: () => Record<string, string>;
1075
1080
  fetch?: typeof fetch;
1081
+ ensureApiKey?: () => Promise<void>;
1076
1082
  refreshApiKey?: () => Promise<void>;
1077
1083
  openaiModel?: string;
1078
1084
  maxTokens?: number;
@@ -1431,6 +1437,14 @@ declare const DEFAULT_AI_GATEWAY_URL = "https://cloud.gitlab.com";
1431
1437
  interface GitLabDirectAccessConfig {
1432
1438
  instanceUrl: string;
1433
1439
  getHeaders: () => Record<string, string>;
1440
+ /**
1441
+ * Optional async hook that resolves once credentials are loaded.
1442
+ * `getHeaders` is synchronous, but the API key is loaded asynchronously; the
1443
+ * client awaits this before building request headers so the first request on
1444
+ * a cold provider doesn't race the lazy key load (which would otherwise throw
1445
+ * "API key is missing" or send an empty Authorization header).
1446
+ */
1447
+ ensureApiKey?: () => Promise<void>;
1434
1448
  fetch?: typeof fetch;
1435
1449
  /**
1436
1450
  * Optional callback to refresh the API key when a 401 error occurs.
package/dist/index.d.ts CHANGED
@@ -6,6 +6,11 @@ interface GitLabAnthropicConfig {
6
6
  instanceUrl: string;
7
7
  getHeaders: () => Record<string, string>;
8
8
  fetch?: typeof fetch;
9
+ /**
10
+ * Optional async hook that resolves once credentials are loaded, awaited
11
+ * before the synchronous getHeaders() is used for the direct-access request.
12
+ */
13
+ ensureApiKey?: () => Promise<void>;
9
14
  /**
10
15
  * Optional callback to refresh the API key when a 401 error occurs.
11
16
  * Should clear cached credentials and re-fetch from auth provider.
@@ -1073,6 +1078,7 @@ interface GitLabOpenAIConfig {
1073
1078
  instanceUrl: string;
1074
1079
  getHeaders: () => Record<string, string>;
1075
1080
  fetch?: typeof fetch;
1081
+ ensureApiKey?: () => Promise<void>;
1076
1082
  refreshApiKey?: () => Promise<void>;
1077
1083
  openaiModel?: string;
1078
1084
  maxTokens?: number;
@@ -1431,6 +1437,14 @@ declare const DEFAULT_AI_GATEWAY_URL = "https://cloud.gitlab.com";
1431
1437
  interface GitLabDirectAccessConfig {
1432
1438
  instanceUrl: string;
1433
1439
  getHeaders: () => Record<string, string>;
1440
+ /**
1441
+ * Optional async hook that resolves once credentials are loaded.
1442
+ * `getHeaders` is synchronous, but the API key is loaded asynchronously; the
1443
+ * client awaits this before building request headers so the first request on
1444
+ * a cold provider doesn't race the lazy key load (which would otherwise throw
1445
+ * "API key is missing" or send an empty Authorization header).
1446
+ */
1447
+ ensureApiKey?: () => Promise<void>;
1434
1448
  fetch?: typeof fetch;
1435
1449
  /**
1436
1450
  * Optional callback to refresh the API key when a 401 error occurs.
package/dist/index.js CHANGED
@@ -727,6 +727,9 @@ var GitLabDirectAccessClient = class {
727
727
  if (this.config.featureFlags && Object.keys(this.config.featureFlags).length > 0) {
728
728
  requestBody.feature_flags = this.config.featureFlags;
729
729
  }
730
+ if (this.config.ensureApiKey) {
731
+ await this.config.ensureApiKey();
732
+ }
730
733
  try {
731
734
  const response = await this.fetchFn(url, {
732
735
  method: "POST",
@@ -822,6 +825,7 @@ var GitLabAnthropicLanguageModel = class {
822
825
  this.directAccessClient = new GitLabDirectAccessClient({
823
826
  instanceUrl: config.instanceUrl,
824
827
  getHeaders: config.getHeaders,
828
+ ensureApiKey: config.ensureApiKey,
825
829
  refreshApiKey: config.refreshApiKey,
826
830
  fetch: config.fetch,
827
831
  featureFlags: config.featureFlags,
@@ -1555,6 +1559,7 @@ var GitLabOpenAILanguageModel = class {
1555
1559
  this.directAccessClient = new GitLabDirectAccessClient({
1556
1560
  instanceUrl: config.instanceUrl,
1557
1561
  getHeaders: config.getHeaders,
1562
+ ensureApiKey: config.ensureApiKey,
1558
1563
  refreshApiKey: config.refreshApiKey,
1559
1564
  fetch: config.fetch,
1560
1565
  featureFlags: config.featureFlags,
@@ -2496,7 +2501,7 @@ var import_node_async_hooks = require("async_hooks");
2496
2501
  var import_isomorphic_ws = __toESM(require("isomorphic-ws"));
2497
2502
 
2498
2503
  // src/version.ts
2499
- var VERSION = true ? "6.9.0" : "0.0.0-dev";
2504
+ var VERSION = true ? "6.9.2" : "0.0.0-dev";
2500
2505
 
2501
2506
  // src/gitlab-workflow-client.ts
2502
2507
  var WS_CONNECT_TIMEOUT_MS = 3e4;
@@ -4833,9 +4838,10 @@ var TOKEN_EXPIRY_SKEW_MS = 5 * 60 * 1e3;
4833
4838
  var OAUTH_SCOPES = ["api"];
4834
4839
 
4835
4840
  // src/gitlab-oauth-manager.ts
4841
+ var globalInFlightRefreshes = /* @__PURE__ */ new Map();
4836
4842
  var GitLabOAuthManager = class {
4837
4843
  fetch;
4838
- inFlightRefreshes = /* @__PURE__ */ new Map();
4844
+ inFlightRefreshes = globalInFlightRefreshes;
4839
4845
  constructor(fetchImpl = fetch) {
4840
4846
  this.fetch = fetchImpl;
4841
4847
  }
@@ -5048,12 +5054,15 @@ async function loadOpenCodeAuth(instanceUrl) {
5048
5054
  }
5049
5055
  const authData = JSON.parse(fs3.readFileSync(authPath, "utf-8"));
5050
5056
  const normalizedUrl = instanceUrl.replace(/\/$/, "");
5051
- if (authData.gitlab?.type === "oauth") {
5052
- const gitlabAuth = authData.gitlab;
5053
- const normalizedEnterpriseUrl = typeof gitlabAuth.enterpriseUrl === "string" ? gitlabAuth.enterpriseUrl.replace(/\/$/, "") : gitlabAuth.enterpriseUrl;
5054
- if (normalizedEnterpriseUrl === normalizedUrl) {
5057
+ const gitlabAuth = authData.gitlab;
5058
+ if (gitlabAuth?.type === "oauth") {
5059
+ const normalizedEnterpriseUrl = typeof gitlabAuth.enterpriseUrl === "string" ? gitlabAuth.enterpriseUrl.replace(/\/$/, "") : void 0;
5060
+ const matchesInstance = normalizedEnterpriseUrl === normalizedUrl || normalizedEnterpriseUrl === void 0 && normalizedUrl === GITLAB_COM_URL;
5061
+ if (matchesInstance) {
5055
5062
  return gitlabAuth;
5056
5063
  }
5064
+ } else if (gitlabAuth?.type === "api" && typeof gitlabAuth.key === "string") {
5065
+ return gitlabAuth;
5057
5066
  }
5058
5067
  const auth = authData[normalizedUrl] || authData[`${normalizedUrl}/`];
5059
5068
  return auth;
@@ -5061,32 +5070,52 @@ async function loadOpenCodeAuth(instanceUrl) {
5061
5070
  return void 0;
5062
5071
  }
5063
5072
  }
5073
+ var inFlightOAuthRefreshes = /* @__PURE__ */ new Map();
5074
+ async function refreshOAuthToken(auth, instanceUrl, clientId) {
5075
+ const existing = inFlightOAuthRefreshes.get(instanceUrl);
5076
+ if (existing) {
5077
+ return existing;
5078
+ }
5079
+ const refreshPromise = (async () => {
5080
+ const oauthManager = new GitLabOAuthManager();
5081
+ const refreshed = await oauthManager.exchangeRefreshToken({
5082
+ instanceUrl,
5083
+ refreshToken: auth.refresh,
5084
+ clientId
5085
+ });
5086
+ const authPath = getOpenCodeAuthPath();
5087
+ const authData = JSON.parse(fs3.readFileSync(authPath, "utf-8"));
5088
+ authData.gitlab = {
5089
+ type: "oauth",
5090
+ refresh: refreshed.refreshToken,
5091
+ access: refreshed.accessToken,
5092
+ expires: refreshed.expiresAt,
5093
+ enterpriseUrl: instanceUrl
5094
+ // Use enterpriseUrl to match auth plugin format
5095
+ };
5096
+ fs3.writeFileSync(authPath, JSON.stringify(authData, null, 2), { mode: 384 });
5097
+ return refreshed.accessToken;
5098
+ })();
5099
+ inFlightOAuthRefreshes.set(instanceUrl, refreshPromise);
5100
+ try {
5101
+ return await refreshPromise;
5102
+ } finally {
5103
+ inFlightOAuthRefreshes.delete(instanceUrl);
5104
+ }
5105
+ }
5064
5106
  async function loadApiKey(options, instanceUrl, clientId) {
5065
5107
  if (options.apiKey) {
5066
5108
  return options.apiKey;
5067
5109
  }
5068
5110
  const auth = await loadOpenCodeAuth(instanceUrl);
5111
+ if (auth?.type === "api") {
5112
+ return auth.key;
5113
+ }
5069
5114
  if (auth?.type === "oauth") {
5070
5115
  const oauthManager = new GitLabOAuthManager();
5071
5116
  if (oauthManager.needsRefresh(auth.expires)) {
5072
5117
  try {
5073
- const refreshed = await oauthManager.exchangeRefreshToken({
5074
- instanceUrl,
5075
- refreshToken: auth.refresh,
5076
- clientId
5077
- });
5078
- const authPath = getOpenCodeAuthPath();
5079
- const authData = JSON.parse(fs3.readFileSync(authPath, "utf-8"));
5080
- authData.gitlab = {
5081
- type: "oauth",
5082
- refresh: refreshed.refreshToken,
5083
- access: refreshed.accessToken,
5084
- expires: refreshed.expiresAt,
5085
- enterpriseUrl: instanceUrl
5086
- // Use enterpriseUrl to match auth plugin format
5087
- };
5088
- fs3.writeFileSync(authPath, JSON.stringify(authData, null, 2), { mode: 384 });
5089
- return refreshed.accessToken;
5118
+ return await refreshOAuthToken(auth, instanceUrl, clientId);
5090
5119
  } catch (error) {
5091
5120
  const refreshErrorMsg = error instanceof Error ? error.message : String(error);
5092
5121
  const envApiKey = process.env[options.environmentVariableName];
@@ -5185,6 +5214,12 @@ function createGitLab(options = {}) {
5185
5214
  `ai-sdk-gitlab/${VERSION}`
5186
5215
  );
5187
5216
  };
5217
+ const ensureApiKey = async () => {
5218
+ try {
5219
+ await getApiKey();
5220
+ } catch {
5221
+ }
5222
+ };
5188
5223
  getApiKey().catch(() => {
5189
5224
  });
5190
5225
  const createAgenticChatModel = (modelId, agenticOptions) => {
@@ -5219,6 +5254,7 @@ function createGitLab(options = {}) {
5219
5254
  provider: `${providerName}.agentic`,
5220
5255
  instanceUrl,
5221
5256
  getHeaders,
5257
+ ensureApiKey,
5222
5258
  refreshApiKey,
5223
5259
  fetch: options.fetch,
5224
5260
  maxTokens: agenticOptions?.maxTokens,