Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 25 additions & 12 deletions packages/angular/cli/src/commands/mcp/tools/doc-search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,32 @@ import { at, iv, k1 } from '../constants';
import { type McpToolContext, declareTool } from './tool-registry';

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The comment mentions support for self-hosted documentation, but ALGOLIA_APP_ID (line 16) remains hardcoded. A self-hosted Algolia index would require a different Application ID in addition to the API key. To fully support the self-hosted use case as described, consider providing an environment variable override for the App ID as well (e.g. NG_DOCS_SEARCH_APP_ID).

References
  1. Avoid placing a comma immediately after abbreviations like 'e.g.' in user-facing messages.

const ALGOLIA_API_E = '34738e8ae1a45e58bbce7b0f9810633d8b727b44a6479cf5e14b6a337148bd50';

/**
* Resolves the Algolia API key to use for documentation search. If the
* `NG_DOCS_SEARCH_API_KEY` environment variable is set to a non-empty value
* it is used verbatim; otherwise the bundled default is used.
*
* Exported for testing.
*/
export function resolveAlgoliaApiKey(): string {
const override = process.env['NG_DOCS_SEARCH_API_KEY'];
if (typeof override === 'string' && override !== '') {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

It is generally safer to trim environment variable values to prevent issues caused by accidental leading or trailing whitespace in the shell configuration.

Suggested change
if (typeof override === 'string' && override !== '') {
if (typeof override === 'string' && override.trim() !== '') {
return override.trim();
}

return override;
}
const dcip = createDecipheriv(
'aes-256-gcm',
(k1 + ALGOLIA_APP_ID).padEnd(32, '^'),
iv,
).setAuthTag(Buffer.from(at, 'base64'));

return dcip.update(ALGOLIA_API_E, 'hex', 'utf-8') + dcip.final('utf-8');
}

/**
* The minimum major version of Angular for which a version-specific documentation index is known to exist.
* Searches for versions older than this will be clamped to this version.
Expand Down Expand Up @@ -129,16 +150,8 @@ function createDocSearchHandler({ logger }: McpToolContext) {

return async ({ query, includeTopContent, version }: DocSearchInput) => {
if (!client) {
const dcip = createDecipheriv(
'aes-256-gcm',
(k1 + ALGOLIA_APP_ID).padEnd(32, '^'),
iv,
).setAuthTag(Buffer.from(at, 'base64'));
const { searchClient } = await import('algoliasearch');
client = searchClient(
ALGOLIA_APP_ID,
dcip.update(ALGOLIA_API_E, 'hex', 'utf-8') + dcip.final('utf-8'),
);
client = searchClient(ALGOLIA_APP_ID, resolveAlgoliaApiKey());
}

let finalSearchedVersion = Math.max(
Expand Down
45 changes: 45 additions & 0 deletions packages/angular/cli/src/commands/mcp/tools/doc-search_spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/

import { resolveAlgoliaApiKey } from './doc-search';

describe('resolveAlgoliaApiKey', () => {
const ENV_VAR = 'NG_DOCS_SEARCH_API_KEY';
let saved: string | undefined;

beforeEach(() => {
saved = process.env[ENV_VAR];
delete process.env[ENV_VAR];
});

afterEach(() => {
if (saved === undefined) {
delete process.env[ENV_VAR];
} else {
process.env[ENV_VAR] = saved;
}
});

it('returns the env var value when set to a non-empty string', () => {
process.env[ENV_VAR] = 'override-key-1234';

expect(resolveAlgoliaApiKey()).toBe('override-key-1234');
});

it('falls back to the bundled default when the env var is unset', () => {
delete process.env[ENV_VAR];

expect(resolveAlgoliaApiKey()).toMatch(/^[0-9a-f]{32}$/);
});

it('falls back to the bundled default when the env var is an empty string', () => {
process.env[ENV_VAR] = '';

expect(resolveAlgoliaApiKey()).toMatch(/^[0-9a-f]{32}$/);
});
});
Loading