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
39 changes: 27 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,34 @@ 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 thread
gn00295120 marked this conversation as resolved.
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.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 +152,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