Skip to content

Commit 957b613

Browse files
committed
feat(@angular/cli): allow Algolia search key override via env var
The MCP `search_documentation` tool currently uses a single bundled Algolia API key for the public Angular documentation index. This works out of the box but gives operators no way to: * substitute a different key for self-hosted documentation indices, * point an internal CI environment at a separate Algolia application to avoid sharing the public rate-limit budget, * test key rotation without rebuilding the CLI. This change adds a `NG_DOCS_SEARCH_API_KEY` environment variable that, when set to a non-empty string, is used verbatim in place of the bundled key. When the variable is absent or empty, behaviour is unchanged — the bundled key continues to be used. The override path is factored into a small `resolveAlgoliaApiKey()` helper so the env-var precedence rules are unit-testable without spinning up the full MCP tool runner. Three Jasmine specs cover: override set, override unset, and override set to empty string. The previous comment block on `ALGOLIA_API_E` is replaced with a short note describing the override mechanism and the operator scenarios it enables.
1 parent 7da255d commit 957b613

2 files changed

Lines changed: 70 additions & 12 deletions

File tree

packages/angular/cli/src/commands/mcp/tools/doc-search.ts

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,32 @@ import { at, iv, k1 } from '../constants';
1414
import { type McpToolContext, declareTool } from './tool-registry';
1515

1616
const ALGOLIA_APP_ID = 'L1XWT2UJ7F';
17-
// https://www.algolia.com/doc/guides/security/api-keys/#search-only-api-key
18-
// This is a search only, rate limited key. It is sent within the URL of the query request.
19-
// This is not the actual key.
17+
// Default Algolia API key used when NG_DOCS_SEARCH_API_KEY is not set.
18+
// Operators (e.g. self-hosted documentation, internal CI, rotation testing)
19+
// can override this by setting NG_DOCS_SEARCH_API_KEY in the environment.
2020
const ALGOLIA_API_E = '34738e8ae1a45e58bbce7b0f9810633d8b727b44a6479cf5e14b6a337148bd50';
2121

22+
/**
23+
* Resolves the Algolia API key to use for documentation search. If the
24+
* `NG_DOCS_SEARCH_API_KEY` environment variable is set to a non-empty value
25+
* it is used verbatim; otherwise the bundled default is used.
26+
*
27+
* Exported for testing.
28+
*/
29+
export function resolveAlgoliaApiKey(): string {
30+
const override = process.env['NG_DOCS_SEARCH_API_KEY'];
31+
if (typeof override === 'string' && override !== '') {
32+
return override;
33+
}
34+
const dcip = createDecipheriv(
35+
'aes-256-gcm',
36+
(k1 + ALGOLIA_APP_ID).padEnd(32, '^'),
37+
iv,
38+
).setAuthTag(Buffer.from(at, 'base64'));
39+
40+
return dcip.update(ALGOLIA_API_E, 'hex', 'utf-8') + dcip.final('utf-8');
41+
}
42+
2243
/**
2344
* The minimum major version of Angular for which a version-specific documentation index is known to exist.
2445
* Searches for versions older than this will be clamped to this version.
@@ -129,16 +150,8 @@ function createDocSearchHandler({ logger }: McpToolContext) {
129150

130151
return async ({ query, includeTopContent, version }: DocSearchInput) => {
131152
if (!client) {
132-
const dcip = createDecipheriv(
133-
'aes-256-gcm',
134-
(k1 + ALGOLIA_APP_ID).padEnd(32, '^'),
135-
iv,
136-
).setAuthTag(Buffer.from(at, 'base64'));
137153
const { searchClient } = await import('algoliasearch');
138-
client = searchClient(
139-
ALGOLIA_APP_ID,
140-
dcip.update(ALGOLIA_API_E, 'hex', 'utf-8') + dcip.final('utf-8'),
141-
);
154+
client = searchClient(ALGOLIA_APP_ID, resolveAlgoliaApiKey());
142155
}
143156

144157
let finalSearchedVersion = Math.max(
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import { resolveAlgoliaApiKey } from './doc-search';
10+
11+
describe('resolveAlgoliaApiKey', () => {
12+
const ENV_VAR = 'NG_DOCS_SEARCH_API_KEY';
13+
let saved: string | undefined;
14+
15+
beforeEach(() => {
16+
saved = process.env[ENV_VAR];
17+
delete process.env[ENV_VAR];
18+
});
19+
20+
afterEach(() => {
21+
if (saved === undefined) {
22+
delete process.env[ENV_VAR];
23+
} else {
24+
process.env[ENV_VAR] = saved;
25+
}
26+
});
27+
28+
it('returns the env var value when set to a non-empty string', () => {
29+
process.env[ENV_VAR] = 'override-key-1234';
30+
31+
expect(resolveAlgoliaApiKey()).toBe('override-key-1234');
32+
});
33+
34+
it('falls back to the bundled default when the env var is unset', () => {
35+
delete process.env[ENV_VAR];
36+
37+
expect(resolveAlgoliaApiKey()).toMatch(/^[0-9a-f]{32}$/);
38+
});
39+
40+
it('falls back to the bundled default when the env var is an empty string', () => {
41+
process.env[ENV_VAR] = '';
42+
43+
expect(resolveAlgoliaApiKey()).toMatch(/^[0-9a-f]{32}$/);
44+
});
45+
});

0 commit comments

Comments
 (0)