From 46ece5d7fd6696fb8637314dfba3e86018683c64 Mon Sep 17 00:00:00 2001 From: jonathanedey Date: Tue, 23 Jun 2026 12:50:33 -0400 Subject: [PATCH 1/4] chore(docs): add custom `@excludeFromDocs` tag to filter generated docs --- etc/firebase-admin.auth.api.md | 8 +++--- etc/firebase-admin.functions.api.md | 4 +-- generate-reports.js | 29 +++++++++++++++++++++ src/auth/auth-config.ts | 8 ++++-- src/auth/base-auth.ts | 4 ++- src/auth/token-verifier.ts | 40 ++++++++++++++++++++--------- src/functions/functions-api.ts | 8 ++++-- tsdoc.json | 9 +++++++ 8 files changed, 87 insertions(+), 23 deletions(-) create mode 100644 tsdoc.json diff --git a/etc/firebase-admin.auth.api.md b/etc/firebase-admin.auth.api.md index 1300fea965..34b5fcf19a 100644 --- a/etc/firebase-admin.auth.api.md +++ b/etc/firebase-admin.auth.api.md @@ -31,7 +31,7 @@ export interface AllowByDefault { // @public export interface AllowByDefaultWrap { allowByDefault: AllowByDefault; - // @alpha (undocumented) + // (undocumented) allowlistOnly?: never; } @@ -42,7 +42,7 @@ export interface AllowlistOnly { // @public export interface AllowlistOnlyWrap { - // @alpha (undocumented) + // (undocumented) allowByDefault?: never; allowlistOnly: AllowlistOnly; } @@ -196,7 +196,7 @@ export abstract class BaseAuth { setCustomUserClaims(uid: string, customUserClaims: object | null): Promise; updateProviderConfig(providerId: string, updatedConfig: UpdateAuthProviderRequest): Promise; updateUser(uid: string, properties: UpdateRequest): Promise; - // @alpha (undocumented) + // (undocumented) _verifyAuthBlockingToken(token: string, audience?: string): Promise; verifyIdToken(idToken: string, checkRevoked?: boolean): Promise; verifySessionCookie(sessionCookie: string, checkRevoked?: boolean): Promise; @@ -257,7 +257,7 @@ export interface CustomStrengthOptionsConfig { requireUppercase?: boolean; } -// @alpha (undocumented) +// @public (undocumented) export interface DecodedAuthBlockingToken { // (undocumented) [key: string]: any; diff --git a/etc/firebase-admin.functions.api.md b/etc/firebase-admin.functions.api.md index 9ddb0a7b27..d4f8f63239 100644 --- a/etc/firebase-admin.functions.api.md +++ b/etc/firebase-admin.functions.api.md @@ -8,7 +8,7 @@ import { Agent } from 'http'; // @public export interface AbsoluteDelivery { - // @alpha (undocumented) + // (undocumented) scheduleDelaySeconds?: never; scheduleTime?: Date; } @@ -16,7 +16,7 @@ export interface AbsoluteDelivery { // @public export interface DelayDelivery { scheduleDelaySeconds?: number; - // @alpha (undocumented) + // (undocumented) scheduleTime?: never; } diff --git a/generate-reports.js b/generate-reports.js index 8d92b0d2b8..80bf1ec22f 100644 --- a/generate-reports.js +++ b/generate-reports.js @@ -73,11 +73,40 @@ async function generateReportForEntryPoint(entryPoint, filePath) { } console.error(`API Extractor completed successfully`); + + // Strip @excludeFromDocs APIs from the generated docModel so they aren't documented in reference docs. + const apiJsonPath = path.resolve('temp', `${safeName}.api.json`); + await stripHiddenDocsFromApiJson(apiJsonPath); } finally { await fs.unlink(tempConfigFile); } } +async function stripHiddenDocsFromApiJson(apiJsonPath) { + if (!await fs.exists(apiJsonPath)) { + return; + } + const content = await fs.readFile(apiJsonPath, 'utf8'); + const data = JSON.parse(content); + + let removedCount = 0; + function removeHidden(node) { + if (node.members) { + const originalLength = node.members.length; + // Filter out any member whose docComment includes @excludeFromDocs + node.members = node.members.filter(m => !(m.docComment && /@excludeFromDocs\b/.test(m.docComment))); + removedCount += (originalLength - node.members.length); + node.members.forEach(removeHidden); + } + } + + removeHidden(data); + if (removedCount > 0) { + console.log(`Removed ${removedCount} @excludeFromDocs items from ${path.basename(apiJsonPath)}`); + await fs.writeFile(apiJsonPath, JSON.stringify(data, null, 2)); + } +} + (async () => { try { await generateReports(); diff --git a/src/auth/auth-config.ts b/src/auth/auth-config.ts index 7e5c1a8b63..b7520f8471 100644 --- a/src/auth/auth-config.ts +++ b/src/auth/auth-config.ts @@ -1631,7 +1631,9 @@ export interface AllowByDefaultWrap { * Allow every region by default. */ allowByDefault: AllowByDefault; - /** @alpha */ + /** + * @excludeFromDocs + */ allowlistOnly?: never; } @@ -1644,7 +1646,9 @@ export interface AllowlistOnlyWrap { * allowlist. */ allowlistOnly: AllowlistOnly; - /** @alpha */ + /** + * @excludeFromDocs + */ allowByDefault?: never; } diff --git a/src/auth/base-auth.ts b/src/auth/base-auth.ts index 8e067776cb..115514bc1c 100644 --- a/src/auth/base-auth.ts +++ b/src/auth/base-auth.ts @@ -1096,7 +1096,9 @@ export abstract class BaseAuth { return Promise.reject(new FirebaseAuthError(authClientErrorCode.INVALID_PROVIDER_ID)); } - /** @alpha */ + /** + * @excludeFromDocs + */ // eslint-disable-next-line @typescript-eslint/naming-convention public _verifyAuthBlockingToken( token: string, diff --git a/src/auth/token-verifier.ts b/src/auth/token-verifier.ts index 112b82fc32..b839b0a3a8 100644 --- a/src/auth/token-verifier.ts +++ b/src/auth/token-verifier.ts @@ -177,7 +177,9 @@ export interface DecodedIdToken { [key: string]: any; } -/** @alpha */ +/** + * @excludeFromDocs + */ export interface DecodedAuthBlockingSharedUserInfo { uid: string; display_name?: string; @@ -186,18 +188,24 @@ export interface DecodedAuthBlockingSharedUserInfo { phone_number?: string; } -/** @alpha */ +/** + * @excludeFromDocs + */ export interface DecodedAuthBlockingMetadata { creation_time?: number; last_sign_in_time?: number; } -/** @alpha */ +/** + * @excludeFromDocs + */ export interface DecodedAuthBlockingUserInfo extends DecodedAuthBlockingSharedUserInfo { provider_id: string; } -/** @alpha */ +/** + * @excludeFromDocs + */ export interface DecodedAuthBlockingMfaInfo { uid: string; display_name?: string; @@ -206,12 +214,16 @@ export interface DecodedAuthBlockingMfaInfo { factor_id?: string; } -/** @alpha */ +/** + * @excludeFromDocs + */ export interface DecodedAuthBlockingEnrolledFactors { enrolled_factors?: DecodedAuthBlockingMfaInfo[]; } -/** @alpha */ +/** + * @excludeFromDocs + */ export interface DecodedAuthBlockingUserRecord extends DecodedAuthBlockingSharedUserInfo { email_verified?: boolean; disabled?: boolean; @@ -226,7 +238,9 @@ export interface DecodedAuthBlockingUserRecord extends DecodedAuthBlockingShared [key: string]: any; } -/** @alpha */ +/** + * @excludeFromDocs + */ export interface DecodedAuthBlockingToken { aud: string; exp: number; @@ -333,7 +347,7 @@ export class FirebaseTokenVerifier { private readonly signatureVerifier: SignatureVerifier; constructor(clientCertUrl: string, private issuer: string, private tokenInfo: FirebaseTokenInfo, - private readonly app: App) { + private readonly app: App) { if (!validator.isURL(clientCertUrl)) { throw new FirebaseAuthError( @@ -410,7 +424,9 @@ export class FirebaseTokenVerifier { }); } - /** @alpha */ + /** + * @excludeFromDocs + */ // eslint-disable-next-line @typescript-eslint/naming-convention public _verifyAuthBlockingToken( jwtToken: string, @@ -448,7 +464,7 @@ export class FirebaseTokenVerifier { ); } return Promise.resolve(projectId); - }) + }); } private decodeAndVerify( @@ -521,10 +537,10 @@ export class FirebaseTokenVerifier { '"' + header.alg + '".' + verifyJwtTokenDocsMessage; } else if (typeof audience !== 'undefined' && !(payload.aud as string).includes(audience)) { errorMessage = `${this.tokenInfo.jwtName} has incorrect "aud" (audience) claim. Expected "` + - audience + '" but got "' + payload.aud + '".' + verifyJwtTokenDocsMessage; + audience + '" but got "' + payload.aud + '".' + verifyJwtTokenDocsMessage; } else if (typeof audience === 'undefined' && payload.aud !== projectId) { errorMessage = `${this.tokenInfo.jwtName} has incorrect "aud" (audience) claim. Expected "` + - projectId + '" but got "' + payload.aud + '".' + projectIdMatchMessage + + projectId + '" but got "' + payload.aud + '".' + projectIdMatchMessage + verifyJwtTokenDocsMessage; } else if (payload.iss !== this.issuer + projectId) { errorMessage = `${this.tokenInfo.jwtName} has incorrect "iss" (issuer) claim. Expected ` + diff --git a/src/functions/functions-api.ts b/src/functions/functions-api.ts index e685aeb99b..e7e1c3e932 100644 --- a/src/functions/functions-api.ts +++ b/src/functions/functions-api.ts @@ -24,7 +24,9 @@ export interface DelayDelivery { * This delay is added to the current time. */ scheduleDelaySeconds?: number; - /** @alpha */ + /** + * @excludeFromDocs + */ scheduleTime?: never; } @@ -36,7 +38,9 @@ export interface AbsoluteDelivery { * The time when the task is scheduled to be attempted or retried. */ scheduleTime?: Date; - /** @alpha */ + /** + * @excludeFromDocs + */ scheduleDelaySeconds?: never; } diff --git a/tsdoc.json b/tsdoc.json new file mode 100644 index 0000000000..59e9b2a81e --- /dev/null +++ b/tsdoc.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json", + "tagDefinitions": [ + { + "tagName": "@excludeFromDocs", + "syntaxKind": "modifier" + } + ] +} \ No newline at end of file From 8ccc2f7ee5fb5c2654cf8e672b19a25072610f59 Mon Sep 17 00:00:00 2001 From: jonathanedey Date: Tue, 14 Jul 2026 18:41:41 -0400 Subject: [PATCH 2/4] chore: fix report file path validation --- generate-reports.js | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/generate-reports.js b/generate-reports.js index 80bf1ec22f..5e94c81372 100644 --- a/generate-reports.js +++ b/generate-reports.js @@ -33,10 +33,25 @@ const config = require('./api-extractor.json'); const tempConfigFile = 'api-extractor.tmp'; +// Regex to validate that the entrypoint name consists of safe alphanumeric, underscore, +// and dash characters, optionally separated by single slashes. +const ENTRY_POINT_REGEX = /^[a-zA-Z0-9_-]+(?:\/[a-zA-Z0-9_-]+)*$/; + +// Regex to validate that the typing file path is a local relative declaration file path +// inside the "./lib" directory. +const TYPING_FILE_PATH_REGEX = /^\.\/lib\/[a-zA-Z0-9_\-\/]+\.d\.ts$/; + async function generateReports() { const entryPoints = require('./entrypoints.json'); for (const entryPoint in entryPoints) { + // Validate entryPoint to prevent path traversal + if (!ENTRY_POINT_REGEX.test(entryPoint)) { + throw new Error(`Invalid entryPoint format: ${entryPoint}`); + } const filePath = entryPoints[entryPoint].typings; + if (filePath.includes('..') || !TYPING_FILE_PATH_REGEX.test(filePath)) { + throw new Error(`Invalid typing file path: ${filePath}`); + } await generateReportForEntryPoint(entryPoint, filePath); } } @@ -46,6 +61,9 @@ async function generateReportForEntryPoint(entryPoint, filePath) { console.log('========================================================\n'); const safeName = entryPoint.replace('/', '.'); + if (safeName.includes('..') || safeName.includes('/') || safeName.includes('\\')) { + throw new Error(`Invalid safeName calculated: ${safeName}`); + } console.log('Updating configuration for entry point...'); config.apiReport.reportFileName = `${safeName}.api.md`; config.mainEntryPointFilePath = filePath; @@ -75,7 +93,12 @@ async function generateReportForEntryPoint(entryPoint, filePath) { console.error(`API Extractor completed successfully`); // Strip @excludeFromDocs APIs from the generated docModel so they aren't documented in reference docs. - const apiJsonPath = path.resolve('temp', `${safeName}.api.json`); + const tempDir = path.resolve('temp'); + const apiJsonPath = path.resolve(tempDir, `${safeName}.api.json`); + const relativePath = path.relative(tempDir, apiJsonPath); + if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) { + throw new Error(`Path traversal detected: ${apiJsonPath}`); + } await stripHiddenDocsFromApiJson(apiJsonPath); } finally { await fs.unlink(tempConfigFile); From e79761c3a86e11101f8b49fa0ee473f391c2c670 Mon Sep 17 00:00:00 2001 From: jonathanedey Date: Wed, 15 Jul 2026 12:55:12 -0400 Subject: [PATCH 3/4] chore: fix paths --- generate-reports.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/generate-reports.js b/generate-reports.js index 5e94c81372..bdf913274a 100644 --- a/generate-reports.js +++ b/generate-reports.js @@ -60,7 +60,7 @@ async function generateReportForEntryPoint(entryPoint, filePath) { console.log(`\nGenerating API report for ${entryPoint}`) console.log('========================================================\n'); - const safeName = entryPoint.replace('/', '.'); + const safeName = entryPoint.replace(/\//g, '.'); if (safeName.includes('..') || safeName.includes('/') || safeName.includes('\\')) { throw new Error(`Invalid safeName calculated: ${safeName}`); } From c72741ede3b53db6b8dc7a4806d78411f87f67a7 Mon Sep 17 00:00:00 2001 From: jonathanedey Date: Wed, 15 Jul 2026 13:22:30 -0400 Subject: [PATCH 4/4] chore: fix paths --- generate-reports.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/generate-reports.js b/generate-reports.js index bdf913274a..2f56990bf7 100644 --- a/generate-reports.js +++ b/generate-reports.js @@ -48,8 +48,12 @@ async function generateReports() { if (!ENTRY_POINT_REGEX.test(entryPoint)) { throw new Error(`Invalid entryPoint format: ${entryPoint}`); } - const filePath = entryPoints[entryPoint].typings; - if (filePath.includes('..') || !TYPING_FILE_PATH_REGEX.test(filePath)) { + const rawFilePath = entryPoints[entryPoint].typings; + if (!TYPING_FILE_PATH_REGEX.test(rawFilePath)) { + throw new Error(`Invalid typing file path: ${rawFilePath}`); + } + const filePath = path.normalize(rawFilePath); + if (filePath.includes('..')) { throw new Error(`Invalid typing file path: ${filePath}`); } await generateReportForEntryPoint(entryPoint, filePath); @@ -60,7 +64,7 @@ async function generateReportForEntryPoint(entryPoint, filePath) { console.log(`\nGenerating API report for ${entryPoint}`) console.log('========================================================\n'); - const safeName = entryPoint.replace(/\//g, '.'); + const safeName = path.basename(entryPoint.replace(/\//g, '.')); if (safeName.includes('..') || safeName.includes('/') || safeName.includes('\\')) { throw new Error(`Invalid safeName calculated: ${safeName}`); }