2025-10-20 00:14:50 +00:00
|
|
|
import { BunSha256Hasher, type PassswordHasher } from "./hasher"
|
|
|
|
|
|
|
|
|
|
export { WebCryptoSha256Hasher } from "./hasher"
|
|
|
|
|
|
2025-10-19 17:05:15 +00:00
|
|
|
/**
|
|
|
|
|
* An unhashed api key.
|
|
|
|
|
* Always starts with sk, then the prefix specified at time of generation,
|
|
|
|
|
* and ends with the base64url-encoded key.
|
|
|
|
|
*/
|
|
|
|
|
export type UnhashedApiKey = `sk-${ApiKeyPrefix}-${string}`
|
|
|
|
|
|
|
|
|
|
export type ApiKeyPrefix = string & { __brand: "ApiKeyPrefix" }
|
|
|
|
|
|
2025-10-20 00:14:50 +00:00
|
|
|
export type ParsedApiKey = {
|
|
|
|
|
prefix: ApiKeyPrefix
|
|
|
|
|
unhashedKey: UnhashedApiKey
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-19 17:05:15 +00:00
|
|
|
export type GenerateApiKeyOptions = {
|
|
|
|
|
/**
|
|
|
|
|
* How long the key should be (excluding prefix) in bytes.
|
|
|
|
|
*/
|
|
|
|
|
keyByteLength: number
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Prefix of the api key. Can be a brief name of the consumer of the key.
|
|
|
|
|
* For example, if the prefix is "proxy", the key will be "sk-proxy-asdasjdjsdkjd.."
|
|
|
|
|
*/
|
|
|
|
|
prefix: ApiKeyPrefix
|
|
|
|
|
|
|
|
|
|
expiresAt?: Date
|
|
|
|
|
description: string
|
2025-10-20 00:14:50 +00:00
|
|
|
|
|
|
|
|
hasher?: PassswordHasher
|
2025-10-19 17:05:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export type GenerateApiKeyResult = {
|
|
|
|
|
unhashedKey: UnhashedApiKey
|
|
|
|
|
hashedKey: string
|
|
|
|
|
expiresAt?: Date
|
|
|
|
|
description: string
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-20 00:14:50 +00:00
|
|
|
export type VerifyApiKeyOptions = {
|
|
|
|
|
keyToBeVerified: UnhashedApiKey
|
|
|
|
|
hashedKey: string
|
|
|
|
|
hasher?: PassswordHasher
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function validatePrefix(prefix: string): prefix is ApiKeyPrefix {
|
|
|
|
|
return !prefix.includes("-")
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-19 17:05:15 +00:00
|
|
|
export function newPrefix(prefix: string): ApiKeyPrefix | null {
|
2025-10-20 00:14:50 +00:00
|
|
|
if (!validatePrefix(prefix)) {
|
2025-10-19 17:05:15 +00:00
|
|
|
return null
|
|
|
|
|
}
|
2025-10-20 00:14:50 +00:00
|
|
|
return prefix
|
2025-10-19 17:05:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function generateApiKey({
|
|
|
|
|
keyByteLength,
|
|
|
|
|
prefix,
|
|
|
|
|
expiresAt,
|
|
|
|
|
description,
|
2025-10-20 00:14:50 +00:00
|
|
|
hasher = new BunSha256Hasher(),
|
2025-10-19 17:05:15 +00:00
|
|
|
}: GenerateApiKeyOptions): Promise<GenerateApiKeyResult> {
|
|
|
|
|
const keyContent = new Uint8Array(keyByteLength)
|
|
|
|
|
crypto.getRandomValues(keyContent)
|
|
|
|
|
|
|
|
|
|
const base64KeyContent = Buffer.from(keyContent).toString("base64url")
|
|
|
|
|
const unhashedKey: UnhashedApiKey = `sk-${prefix}-${base64KeyContent}`
|
|
|
|
|
|
2025-10-20 00:14:50 +00:00
|
|
|
const hashedKey = await hasher.hash(unhashedKey)
|
2025-10-19 17:05:15 +00:00
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
unhashedKey,
|
|
|
|
|
hashedKey,
|
|
|
|
|
expiresAt,
|
|
|
|
|
description,
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-10-20 00:14:50 +00:00
|
|
|
|
|
|
|
|
export function parseApiKey(key: string): ParsedApiKey | null {
|
|
|
|
|
if (!key.startsWith("sk-")) {
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
const parts = key.split("-")
|
|
|
|
|
if (parts.length !== 3) {
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
const prefix = parts[1]
|
|
|
|
|
if (!prefix || !validatePrefix(prefix)) {
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
prefix,
|
|
|
|
|
unhashedKey: key as UnhashedApiKey,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function verifyApiKey({
|
|
|
|
|
keyToBeVerified,
|
|
|
|
|
hashedKey,
|
|
|
|
|
hasher = new BunSha256Hasher(),
|
|
|
|
|
}: VerifyApiKeyOptions): Promise<boolean> {
|
|
|
|
|
return await hasher.verify(keyToBeVerified, hashedKey)
|
|
|
|
|
}
|