th3w1zard1's picture
Deploy GPTR trask-http from community-bots local CI
3a25f97 verified
import { extractBearerToken, requireBearerToken } from "./bearer-tokens.js";
export interface AuthResolver<TContext, TResolved> {
readonly name?: string;
resolve(context: TContext): Promise<TResolved | null> | TResolved | null;
}
export interface BearerAuthContext {
authHeader: string | undefined;
token: string;
}
export interface SessionResolverResult<TAccount, TSession> {
account: TAccount;
session: TSession;
}
export interface ExternalAccountInput {
providerUserId: string;
username: string;
displayName: string;
}
export interface ExternalAccountResult<TAccount> {
account: TAccount;
}
export interface CreateSessionAuthResolverOptions<TAccount, TSession, TResolved> {
name?: string;
resolveSessionToken: (token: string) => Promise<SessionResolverResult<TAccount, TSession> | undefined>;
mapResolvedSession: (resolved: SessionResolverResult<TAccount, TSession>) => TResolved;
}
export interface CreateDevBearerAuthResolverOptions<TAccount, TResolved> {
name?: string;
enabled?: boolean;
prefix?: string;
decodeUserId?: (rawId: string) => string;
createUsername?: (providerUserId: string) => string;
createDisplayName?: (providerUserId: string) => string;
ensureAccount: (input: ExternalAccountInput) => Promise<ExternalAccountResult<TAccount>>;
mapResolvedAccount: (resolved: ExternalAccountInput & { account: TAccount }) => TResolved;
}
export interface CreateHeaderProfileAuthResolverOptions<TProfile, TAccount, TResolved> {
name?: string;
resolveProfile: (authHeader: string | undefined) => Promise<TProfile>;
ensureAccount: (input: ExternalAccountInput) => Promise<ExternalAccountResult<TAccount>>;
getProviderUserId: (profile: TProfile) => string;
getUsername: (profile: TProfile) => string;
getDisplayName: (profile: TProfile) => string;
mapResolvedAccount: (resolved: { account: TAccount; profile: TProfile; providerUserId: string; username: string; displayName: string }) => TResolved;
}
/** Normalized error passed to {@link AuthRequestHandlerFactoryOptions.sendError} after auth handler catches a thrown value. */
export interface NormalizedAuthHandlerError {
readonly status: number;
readonly message: string;
}
/**
* Superset of values commonly thrown or passed through auth handlers.
* ECMAScript `catch` bindings are not statically typed; narrow at the call site with `error as AuthHandlerThrown`.
*/
export type AuthHandlerThrown =
| string
| number
| bigint
| boolean
| symbol
| null
| undefined
| object;
/** Converts a thrown catch value into a stable shape for HTTP responses. */
export const normalizeAuthHandlerError = (error: AuthHandlerThrown): NormalizedAuthHandlerError => {
let status = 500;
if (typeof error === "object" && error !== null) {
const candidate = Reflect.get(error, "status");
if (typeof candidate === "number" && Number.isFinite(candidate)) {
status = candidate;
}
}
const message =
error instanceof Error
? error.message
: typeof error === "string"
? error
: String(error);
return { status, message };
};
export interface AuthRequestHandlerFactoryOptions<TRequest, TResponse, TResolved> {
resolvers: readonly AuthResolver<BearerAuthContext, TResolved>[];
invalidMessage?: string;
getAuthHeader: (request: TRequest) => string | undefined;
sendError: (response: TResponse, error: NormalizedAuthHandlerError) => void | Promise<void>;
}
export type RequiredAuthHandler<TRequest, TResponse, TResolved> = (
request: TRequest,
response: TResponse,
resolved: TResolved,
) => void | Promise<void>;
export type OptionalAuthHandler<TRequest, TResponse, TResolved> = (
request: TRequest,
response: TResponse,
resolved: TResolved | null,
) => void | Promise<void>;
const defaultDevUserIdDecoder = (rawId: string): string => decodeURIComponent(rawId);
const defaultDevUsername = (providerUserId: string): string => {
return providerUserId.replace(/[^a-zA-Z0-9._-]/gu, "_").slice(0, 32) || "devuser";
};
export const createBearerAuthContext = (authHeader: string | undefined): BearerAuthContext => ({
authHeader,
token: requireBearerToken(authHeader),
});
export const resolveWithAuthResolvers = async <TContext, TResolved>(
context: TContext,
resolvers: readonly AuthResolver<TContext, TResolved>[],
): Promise<TResolved | null> => {
for (const resolver of resolvers) {
const resolved = await resolver.resolve(context);
if (resolved !== null) {
return resolved;
}
}
return null;
};
export const createSessionAuthResolver = <TAccount, TSession, TResolved>(
options: CreateSessionAuthResolverOptions<TAccount, TSession, TResolved>,
): AuthResolver<BearerAuthContext, TResolved> => {
return {
...(options.name ? { name: options.name } : { name: "session" }),
resolve: async ({ token }) => {
const resolved = await options.resolveSessionToken(token);
return resolved ? options.mapResolvedSession(resolved) : null;
},
};
};
export const createDevBearerAuthResolver = <TAccount, TResolved>(
options: CreateDevBearerAuthResolverOptions<TAccount, TResolved>,
): AuthResolver<BearerAuthContext, TResolved> => {
const prefix = options.prefix ?? "dev-user-";
const decodeUserId = options.decodeUserId ?? defaultDevUserIdDecoder;
const createUsername = options.createUsername ?? defaultDevUsername;
const createDisplayName = options.createDisplayName ?? ((providerUserId: string) => providerUserId);
return {
...(options.name ? { name: options.name } : { name: "dev" }),
resolve: async ({ authHeader }) => {
if (!options.enabled) {
return null;
}
const token = extractBearerToken(authHeader);
if (!token || !token.startsWith(prefix)) {
return null;
}
const rawId = token.slice(prefix.length).trim();
if (!rawId) {
throw Object.assign(new Error("Invalid dev auth token format."), { status: 401 });
}
const providerUserId = decodeUserId(rawId);
const username = createUsername(providerUserId);
const displayName = createDisplayName(providerUserId);
const { account } = await options.ensureAccount({ providerUserId, username, displayName });
return options.mapResolvedAccount({ account, providerUserId, username, displayName });
},
};
};
export const createHeaderProfileAuthResolver = <TProfile, TAccount, TResolved>(
options: CreateHeaderProfileAuthResolverOptions<TProfile, TAccount, TResolved>,
): AuthResolver<BearerAuthContext, TResolved> => {
return {
...(options.name ? { name: options.name } : {}),
resolve: async ({ authHeader }) => {
const profile = await options.resolveProfile(authHeader);
const providerUserId = options.getProviderUserId(profile);
const username = options.getUsername(profile);
const displayName = options.getDisplayName(profile);
const { account } = await options.ensureAccount({ providerUserId, username, displayName });
return options.mapResolvedAccount({
account,
profile,
providerUserId,
username,
displayName,
});
},
};
};
export const resolveRequiredWithAuthResolvers = async <TResolved>(
authHeader: string | undefined,
resolvers: readonly AuthResolver<BearerAuthContext, TResolved>[],
invalidMessage = "Unauthorized.",
): Promise<TResolved> => {
const context = createBearerAuthContext(authHeader);
const resolved = await resolveWithAuthResolvers(context, resolvers);
if (resolved === null) {
throw Object.assign(new Error(invalidMessage), { status: 401 });
}
return resolved;
};
export const resolveOptionalWithAuthResolvers = async <TResolved>(
authHeader: string | undefined,
resolvers: readonly AuthResolver<BearerAuthContext, TResolved>[],
invalidMessage = "Unauthorized.",
): Promise<TResolved | null> => {
try {
return await resolveRequiredWithAuthResolvers(authHeader, resolvers, invalidMessage);
} catch {
return null;
}
};
export const createRequiredAuthHandler = <TRequest, TResponse, TResolved>(
options: AuthRequestHandlerFactoryOptions<TRequest, TResponse, TResolved>,
handler: RequiredAuthHandler<TRequest, TResponse, TResolved>,
): ((request: TRequest, response: TResponse) => Promise<void>) => {
return async (request, response): Promise<void> => {
try {
const resolved = await resolveRequiredWithAuthResolvers(
options.getAuthHeader(request),
options.resolvers,
options.invalidMessage,
);
await handler(request, response, resolved);
} catch (error) {
await options.sendError(response, normalizeAuthHandlerError(error as AuthHandlerThrown));
}
};
};
export const createOptionalAuthHandler = <TRequest, TResponse, TResolved>(
options: AuthRequestHandlerFactoryOptions<TRequest, TResponse, TResolved>,
handler: OptionalAuthHandler<TRequest, TResponse, TResolved>,
): ((request: TRequest, response: TResponse) => Promise<void>) => {
return async (request, response): Promise<void> => {
try {
const resolved = await resolveOptionalWithAuthResolvers(
options.getAuthHeader(request),
options.resolvers,
options.invalidMessage,
);
await handler(request, response, resolved);
} catch (error) {
await options.sendError(response, normalizeAuthHandlerError(error as AuthHandlerThrown));
}
};
};