Spaces:
Running
Running
File size: 3,237 Bytes
98c9143 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 | import type { Context, MiddlewareHandler } from "hono"
import consola from "consola"
import { getConfig } from "./config"
interface AuthMiddlewareOptions {
getApiKeys?: () => Array<string>
allowUnauthenticatedPaths?: Array<string>
allowOptionsBypass?: boolean
allowWhenNoApiKeys?: boolean
shouldSkipPath?: (path: string) => boolean
}
export function normalizeApiKeys(apiKeys: unknown): Array<string> {
if (!Array.isArray(apiKeys)) {
if (apiKeys !== undefined) {
consola.warn("Invalid auth.apiKeys config. Expected an array of strings.")
}
return []
}
const normalizedKeys = apiKeys
.filter((key): key is string => typeof key === "string")
.map((key) => key.trim())
.filter((key) => key.length > 0)
if (normalizedKeys.length !== apiKeys.length) {
consola.warn(
"Invalid auth.apiKeys entries found. Only non-empty strings are allowed.",
)
}
return [...new Set(normalizedKeys)]
}
export function getConfiguredApiKeys(): Array<string> {
const config = getConfig()
return normalizeApiKeys(config.auth?.apiKeys)
}
function normalizeApiKey(apiKey: unknown): string | null {
if (typeof apiKey !== "string") {
return null
}
const normalizedApiKey = apiKey.trim()
return normalizedApiKey || null
}
export function getConfiguredAdminApiKeys(): Array<string> {
const config = getConfig()
const adminApiKey = normalizeApiKey(config.auth?.adminApiKey)
return adminApiKey ? [adminApiKey] : []
}
export function extractRequestApiKey(c: Context): string | null {
const xApiKey = c.req.header("x-api-key")?.trim()
if (xApiKey) {
return xApiKey
}
const authorization = c.req.header("authorization")
if (!authorization) {
return null
}
const [scheme, ...rest] = authorization.trim().split(/\s+/)
if (scheme.toLowerCase() !== "bearer") {
return null
}
const bearerToken = rest.join(" ").trim()
return bearerToken || null
}
function createUnauthorizedResponse(c: Context): Response {
c.header("WWW-Authenticate", 'Bearer realm="copilot-api"')
return c.json(
{
error: {
message: "Unauthorized",
type: "authentication_error",
},
},
401,
)
}
export function createAuthMiddleware(
options: AuthMiddlewareOptions = {},
): MiddlewareHandler {
const getApiKeys = options.getApiKeys ?? getConfiguredApiKeys
const allowUnauthenticatedPaths = options.allowUnauthenticatedPaths ?? ["/"]
const allowOptionsBypass = options.allowOptionsBypass ?? true
const allowWhenNoApiKeys = options.allowWhenNoApiKeys ?? true
const shouldSkipPath = options.shouldSkipPath ?? (() => false)
return async (c, next) => {
if (allowOptionsBypass && c.req.method === "OPTIONS") {
return next()
}
if (shouldSkipPath(c.req.path)) {
return next()
}
if (allowUnauthenticatedPaths.includes(c.req.path)) {
return next()
}
const apiKeys = getApiKeys()
if (apiKeys.length === 0) {
return allowWhenNoApiKeys ? next() : createUnauthorizedResponse(c)
}
const requestApiKey = extractRequestApiKey(c)
if (!requestApiKey || !apiKeys.includes(requestApiKey)) {
return createUnauthorizedResponse(c)
}
return next()
}
}
|