const internalApiKeyAlphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" const internalApiKeyPrefix = "sk-" const internalApiKeyBodyLength = 48 interface InternalApiKeyFormat { prefix: string alphabet: string bodyLength: number } // API key 的生成依赖安全随机源。 // 这个辅助函数同时负责长度校验和随机字节填充,使 API key 生成逻辑可以直接建立在“安全随机字节”之上。 const internalGetApiKeyRandomValues = (length: number): Uint8Array => { if (Number.isInteger(length) === false || length <= 0) { throw new RangeError(`Expected length to be a positive integer, got: ${length}`) } if (globalThis.crypto === undefined || typeof globalThis.crypto.getRandomValues !== "function") { throw new Error("Credential utilities require crypto.getRandomValues") } const buffer = new Uint8Array(length) globalThis.crypto.getRandomValues(buffer) return buffer } // API key 的生成与校验必须共享同一份格式定义。 // 这个辅助函数负责补齐默认值并统一校验前缀、字符集和主体长度,避免生成与遮罩对“合法 key”的理解出现分叉。 const internalResolveApiKeyFormat = ( options: GenerateApiKeyOptions | undefined, ): InternalApiKeyFormat => { const prefix = options?.prefix ?? internalApiKeyPrefix const alphabet = options?.alphabet ?? internalApiKeyAlphabet const bodyLength = options?.bodyLength ?? internalApiKeyBodyLength if (prefix.length === 0) { throw new RangeError("Expected prefix to contain at least one character") } if (alphabet.length === 0) { throw new RangeError("Expected alphabet to contain at least one character") } if (Number.isInteger(bodyLength) === false || bodyLength <= 0) { throw new RangeError(`Expected bodyLength to be a positive integer, got: ${bodyLength}`) } return { prefix, alphabet, bodyLength, } } // Step 1: 校验输入值是否满足 API key 格式约定。 // // 遮罩 API key 时需要按固定位置保留前缀和尾部字符,因此必须先确认输入值符合预期长度和前缀规则。 const internalAssertApiKey = (apiKey: string, format: InternalApiKeyFormat): void => { const expectedLength = format.prefix.length + format.bodyLength if (apiKey.startsWith(format.prefix) === false || apiKey.length !== expectedLength) { throw new Error("Invalid API key format") } const body = apiKey.slice(format.prefix.length) for (const character of body) { if (format.alphabet.includes(character) === false) { throw new Error("Invalid API key format") } } } // Step 2: 生成 API key 的随机主体。 // // 该实现直接把安全随机字节映射为 API key 字符,避免把凭据格式语义拆散到通用随机能力之外。 // 读者只需要理解一个规则:每个随机字节都会被转换成允许字符集中的一个字符。 const internalGenerateApiKeyBody = (alphabet: string, bodyLength: number): string => { const randomValues = internalGetApiKeyRandomValues(bodyLength) let result = "" for (const value of randomValues) { result = result + alphabet[value % alphabet.length]! } return result } /** * @description API key 生成选项。 * * `prefix` 用于指定凭据前缀,`alphabet` 用于指定主体字符集,`bodyLength` 用于指定主体长度。 * 未提供的字段会分别回退到默认前缀、默认字符集和默认主体长度。 */ export interface GenerateApiKeyOptions { prefix?: string | undefined alphabet?: string | undefined bodyLength?: number | undefined } /** * @description 生成 API key 字符串,并允许调用方覆写前缀、字符集和主体长度。 * * 该函数使用运行时提供的安全随机源生成固定长度的凭据文本,适合在需要不透明访问凭据的边界层生成新 key。 * * @example * ``` * const apiKey = generateApiKey({}) * // Expect: true * const example1 = apiKey.startsWith("sk-") * // Expect: 51 * const example2 = apiKey.length * const customApiKey = generateApiKey({ prefix: "pk_", alphabet: "ABC123", bodyLength: 8 }) * // Expect: true * const example3 = customApiKey.startsWith("pk_") * // Expect: 11 * const example4 = customApiKey.length * ``` */ export const generateApiKey = (options: GenerateApiKeyOptions): string => { const { prefix, alphabet, bodyLength } = internalResolveApiKeyFormat(options) return `${prefix}${internalGenerateApiKeyBody(alphabet, bodyLength)}` } /** * @description 遮罩 API key 的中间部分,保留前缀与末尾少量可见字符。 * * 当输入值不满足当前 API key 格式约定时,该函数会抛出 Error。 * 若 API key 使用了非默认前缀、字符集或主体长度,应传入同一份格式选项以保持生成与遮罩语义一致。 * * @example * ``` * // Expect: "sk-ab********************************************yz" * const example1 = maskApiKey("sk-ab0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklyz") * const customOptions = { prefix: "pk_", alphabet: "ABC123", bodyLength: 6 } * // Expect: "pk_AB**23" * const example2 = maskApiKey("pk_ABC123", customOptions) * // Expect: throws Error * const example3 = () => maskApiKey("not-an-api-key") * ``` */ export const maskApiKey = (apiKey: string, options?: GenerateApiKeyOptions): string => { const format = internalResolveApiKeyFormat(options) internalAssertApiKey(apiKey, format) // 保留前缀可以帮助识别凭据类型,保留末尾少量字符可以帮助区分不同的 key。 // 遮罩中间主体可以降低 API key 在日志、报错信息或界面中被完整泄露的风险。 const visibleBodyStartLength = Math.min(2, format.bodyLength) const visibleBodyEndLength = Math.min(2, Math.max(0, format.bodyLength - visibleBodyStartLength)) const visibleStart = apiKey.slice(0, format.prefix.length + visibleBodyStartLength) const visibleEnd = visibleBodyEndLength === 0 ? "" : apiKey.slice(-visibleBodyEndLength) const maskedPart = "*".repeat( Math.max(0, format.bodyLength - visibleBodyStartLength - visibleBodyEndLength), ) return `${visibleStart}${maskedPart}${visibleEnd}` }