@nubase/backend 0.1.14 → 0.1.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +16 -1
- package/dist/index.d.ts +16 -1
- package/dist/index.js +30 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +28 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -2
package/dist/index.d.mts
CHANGED
|
@@ -519,4 +519,19 @@ declare function parseCookies(cookieHeader: string): Record<string, string>;
|
|
|
519
519
|
*/
|
|
520
520
|
declare function getCookie(cookieHeader: string, name: string): string | null;
|
|
521
521
|
|
|
522
|
-
|
|
522
|
+
interface ValidateEnvironmentOptions {
|
|
523
|
+
/**
|
|
524
|
+
* The database URL to validate connectivity against.
|
|
525
|
+
* Defaults to process.env.DATABASE_URL
|
|
526
|
+
*/
|
|
527
|
+
databaseUrl?: string;
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Validates that the environment is properly configured and database is accessible.
|
|
531
|
+
* Should be called at application startup before starting the server.
|
|
532
|
+
*
|
|
533
|
+
* @throws Error if DATABASE_URL is not defined or database is not accessible
|
|
534
|
+
*/
|
|
535
|
+
declare function validateEnvironment(options?: ValidateEnvironmentOptions): Promise<void>;
|
|
536
|
+
|
|
537
|
+
export { AUTH_CONTROLLER_KEY, AUTH_USER_KEY, type AuthHandlers, type AuthLevel, type AuthMiddlewareOptions, type AuthVariables, type BackendAuthController, type BackendUser, type CreateAuthHandlersOptions, type CreateHttpHandlerOptions, HttpError, type HttpHandler, type HttpHandlers, type TokenPayload, type TypedHandler, type TypedHandlerContext, type TypedRouteDefinition, type TypedRoutes, type ValidateEnvironmentOptions, type VerifyTokenResult, createAuthHandlers, createAuthMiddleware, createHttpHandler, createTypedHandler, createTypedRoutes, getAuthController, getCookie, getUser, parseCookies, registerHandlers, requireAuth, validateEnvironment };
|
package/dist/index.d.ts
CHANGED
|
@@ -519,4 +519,19 @@ declare function parseCookies(cookieHeader: string): Record<string, string>;
|
|
|
519
519
|
*/
|
|
520
520
|
declare function getCookie(cookieHeader: string, name: string): string | null;
|
|
521
521
|
|
|
522
|
-
|
|
522
|
+
interface ValidateEnvironmentOptions {
|
|
523
|
+
/**
|
|
524
|
+
* The database URL to validate connectivity against.
|
|
525
|
+
* Defaults to process.env.DATABASE_URL
|
|
526
|
+
*/
|
|
527
|
+
databaseUrl?: string;
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Validates that the environment is properly configured and database is accessible.
|
|
531
|
+
* Should be called at application startup before starting the server.
|
|
532
|
+
*
|
|
533
|
+
* @throws Error if DATABASE_URL is not defined or database is not accessible
|
|
534
|
+
*/
|
|
535
|
+
declare function validateEnvironment(options?: ValidateEnvironmentOptions): Promise<void>;
|
|
536
|
+
|
|
537
|
+
export { AUTH_CONTROLLER_KEY, AUTH_USER_KEY, type AuthHandlers, type AuthLevel, type AuthMiddlewareOptions, type AuthVariables, type BackendAuthController, type BackendUser, type CreateAuthHandlersOptions, type CreateHttpHandlerOptions, HttpError, type HttpHandler, type HttpHandlers, type TokenPayload, type TypedHandler, type TypedHandlerContext, type TypedRouteDefinition, type TypedRoutes, type ValidateEnvironmentOptions, type VerifyTokenResult, createAuthHandlers, createAuthMiddleware, createHttpHandler, createTypedHandler, createTypedRoutes, getAuthController, getCookie, getUser, parseCookies, registerHandlers, requireAuth, validateEnvironment };
|
package/dist/index.js
CHANGED
|
@@ -33,7 +33,8 @@ __export(index_exports, {
|
|
|
33
33
|
getUser: () => getUser,
|
|
34
34
|
parseCookies: () => parseCookies,
|
|
35
35
|
registerHandlers: () => registerHandlers,
|
|
36
|
-
requireAuth: () => requireAuth
|
|
36
|
+
requireAuth: () => requireAuth,
|
|
37
|
+
validateEnvironment: () => validateEnvironment
|
|
37
38
|
});
|
|
38
39
|
module.exports = __toCommonJS(index_exports);
|
|
39
40
|
|
|
@@ -158,7 +159,9 @@ function createTypedHandlerInternal(schema, handler, options) {
|
|
|
158
159
|
}
|
|
159
160
|
let params;
|
|
160
161
|
try {
|
|
161
|
-
const
|
|
162
|
+
const pathParams = c.req.param();
|
|
163
|
+
const queryParams = c.req.query();
|
|
164
|
+
const rawParams = { ...pathParams, ...queryParams };
|
|
162
165
|
params = schema.requestParams.toZodWithCoercion().parse(rawParams);
|
|
163
166
|
} catch (error) {
|
|
164
167
|
return c.json(
|
|
@@ -272,6 +275,29 @@ function getCookie(cookieHeader, name) {
|
|
|
272
275
|
const cookies = parseCookies(cookieHeader);
|
|
273
276
|
return cookies[name] ?? null;
|
|
274
277
|
}
|
|
278
|
+
|
|
279
|
+
// src/utils/validate-environment.ts
|
|
280
|
+
var import_pg = require("pg");
|
|
281
|
+
async function validateEnvironment(options = {}) {
|
|
282
|
+
const databaseUrl = options.databaseUrl ?? process.env.DATABASE_URL;
|
|
283
|
+
if (!databaseUrl) {
|
|
284
|
+
throw new Error(
|
|
285
|
+
"DATABASE_URL is not defined in the environment variables."
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
const client = new import_pg.Client({ connectionString: databaseUrl });
|
|
289
|
+
try {
|
|
290
|
+
await client.connect();
|
|
291
|
+
await client.query("SELECT 1");
|
|
292
|
+
await client.end();
|
|
293
|
+
} catch (err) {
|
|
294
|
+
await client.end().catch(() => {
|
|
295
|
+
});
|
|
296
|
+
throw new Error(
|
|
297
|
+
`Failed to connect to database: ${err instanceof Error ? err.message : String(err)}`
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
275
301
|
// Annotate the CommonJS export names for ESM import in node:
|
|
276
302
|
0 && (module.exports = {
|
|
277
303
|
AUTH_CONTROLLER_KEY,
|
|
@@ -287,6 +313,7 @@ function getCookie(cookieHeader, name) {
|
|
|
287
313
|
getUser,
|
|
288
314
|
parseCookies,
|
|
289
315
|
registerHandlers,
|
|
290
|
-
requireAuth
|
|
316
|
+
requireAuth,
|
|
317
|
+
validateEnvironment
|
|
291
318
|
});
|
|
292
319
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/auth/handlers.ts","../src/auth/middleware.ts","../src/typed-handlers.ts","../src/utils/cookies.ts"],"sourcesContent":["export * from \"./auth\";\nexport * from \"./typed-handlers\";\nexport * from \"./utils\";\n","import type { Context } from \"hono\";\nimport { Hono } from \"hono\";\nimport { AUTH_USER_KEY } from \"./middleware\";\nimport type { BackendAuthController, BackendUser } from \"./types\";\n\n/**\n * Options for creating auth handlers.\n */\nexport interface CreateAuthHandlersOptions<\n TUser extends BackendUser = BackendUser,\n> {\n /**\n * The auth controller instance.\n */\n controller: BackendAuthController<TUser>;\n}\n\n/**\n * Auth handlers returned by createAuthHandlers.\n */\nexport interface AuthHandlers {\n /**\n * Login handler - validates credentials and sets auth cookie.\n * Expects JSON body: { username: string, password: string }\n * Returns: { user: { id, email, username } }\n */\n login: (ctx: Context) => Promise<Response>;\n\n /**\n * Logout handler - clears the auth cookie.\n * Returns: { success: true }\n */\n logout: (ctx: Context) => Promise<Response>;\n\n /**\n * Get current user handler - returns the authenticated user or undefined.\n * Returns: { user?: { id, email, username } }\n */\n getMe: (ctx: Context) => Promise<Response>;\n\n /**\n * Pre-configured Hono router with all auth routes.\n * Mount this at /auth to get /auth/login, /auth/logout, /auth/me\n */\n routes: Hono;\n}\n\n/**\n * Create standard authentication handlers from a BackendAuthController.\n *\n * This utility reduces boilerplate by providing pre-built handlers for\n * login, logout, and get-current-user endpoints.\n *\n * @example\n * ```typescript\n * import { createAuthHandlers, createAuthMiddleware } from \"@nubase/backend\";\n *\n * const authController = new MyBackendAuthController();\n * const authHandlers = createAuthHandlers({ controller: authController });\n *\n * const app = new Hono();\n * app.use(\"*\", createAuthMiddleware({ controller: authController }));\n *\n * // Option 1: Register routes individually\n * app.post(\"/auth/login\", authHandlers.login);\n * app.post(\"/auth/logout\", authHandlers.logout);\n * app.get(\"/auth/me\", authHandlers.getMe);\n *\n * // Option 2: Mount the pre-configured router\n * app.route(\"/auth\", authHandlers.routes);\n * ```\n */\nexport function createAuthHandlers<TUser extends BackendUser = BackendUser>(\n options: CreateAuthHandlersOptions<TUser>,\n): AuthHandlers {\n const { controller } = options;\n\n const login = async (ctx: Context): Promise<Response> => {\n try {\n const body = await ctx.req.json<{ username: string; password: string }>();\n\n if (!body.username || !body.password) {\n return ctx.json({ error: \"Username and password are required\" }, 400);\n }\n\n const user = await controller.validateCredentials(\n body.username,\n body.password,\n );\n\n if (!user) {\n return ctx.json({ error: \"Invalid username or password\" }, 401);\n }\n\n const token = await controller.createToken(user);\n controller.setTokenInResponse(ctx, token);\n\n return ctx.json({ user }, 201);\n } catch (error) {\n console.error(\"Login error:\", error);\n return ctx.json({ error: \"Login failed\" }, 500);\n }\n };\n\n const logout = async (ctx: Context): Promise<Response> => {\n controller.clearTokenFromResponse(ctx);\n return ctx.json({ success: true }, 200);\n };\n\n const getMe = async (ctx: Context): Promise<Response> => {\n const user = ctx.get(AUTH_USER_KEY) as TUser | null;\n\n if (!user) {\n return ctx.json({ user: undefined }, 200);\n }\n\n return ctx.json({ user }, 200);\n };\n\n // Create pre-configured router\n const routes = new Hono();\n routes.post(\"/login\", login);\n routes.post(\"/logout\", logout);\n routes.get(\"/me\", getMe);\n\n return {\n login,\n logout,\n getMe,\n routes,\n };\n}\n","import type { Context } from \"hono\";\nimport { createMiddleware } from \"hono/factory\";\nimport type { AuthLevel, BackendAuthController, BackendUser } from \"./types\";\n\n/**\n * Context key for storing the authenticated user.\n * Use `c.get('user')` to retrieve the user in handlers.\n */\nexport const AUTH_USER_KEY = \"user\";\n\n/**\n * Context key for storing the auth controller instance.\n */\nexport const AUTH_CONTROLLER_KEY = \"authController\";\n\n/**\n * Variables added to Hono context by auth middleware.\n */\nexport interface AuthVariables<TUser extends BackendUser = BackendUser> {\n user: TUser | null;\n authController: BackendAuthController<TUser>;\n}\n\n/**\n * Options for the auth middleware.\n */\nexport interface AuthMiddlewareOptions<\n TUser extends BackendUser = BackendUser,\n> {\n /**\n * The auth controller instance to use for token verification.\n */\n controller: BackendAuthController<TUser>;\n\n /**\n * Default auth level for all routes.\n * Can be overridden per-route using createHttpHandler's auth option.\n * @default \"none\"\n */\n defaultAuthLevel?: AuthLevel;\n}\n\n/**\n * Create an authentication middleware for Hono.\n *\n * This middleware:\n * 1. Extracts the token from the request\n * 2. Verifies the token using the provided controller\n * 3. Sets the user in the context (or null if not authenticated)\n * 4. Makes the controller available in context for handlers\n *\n * @example\n * ```typescript\n * const authController = new QuestlogBackendAuthController();\n * const app = new Hono();\n *\n * // Apply to all routes\n * app.use('*', createAuthMiddleware({ controller: authController }));\n *\n * // Access user in handlers\n * app.get('/me', (c) => {\n * const user = c.get('user');\n * if (!user) return c.json({ error: 'Unauthorized' }, 401);\n * return c.json({ user });\n * });\n * ```\n */\nexport function createAuthMiddleware<TUser extends BackendUser = BackendUser>(\n options: AuthMiddlewareOptions<TUser>,\n) {\n const { controller } = options;\n\n return createMiddleware<{ Variables: AuthVariables<TUser> }>(\n async (c, next) => {\n // Store the controller in context for use by handlers\n c.set(AUTH_CONTROLLER_KEY, controller);\n\n // Extract token from request\n const token = controller.extractToken(c);\n\n // Debug logging\n const cookieHeader = c.req.header(\"Cookie\");\n console.info(\n `[Auth] ${c.req.method} ${c.req.path} - Cookie header: ${cookieHeader ? `${cookieHeader.substring(0, 50)}...` : \"(none)\"}`,\n );\n console.info(`[Auth] Token extracted: ${token ? \"yes\" : \"no\"}`);\n\n if (!token) {\n // No token present - user is not authenticated\n c.set(AUTH_USER_KEY, null);\n return next();\n }\n\n // Verify the token\n const result = await controller.verifyToken(token);\n\n console.info(\n `[Auth] Token verification: ${result.valid ? \"valid\" : `invalid - ${result.error}`}`,\n );\n\n if (result.valid) {\n c.set(AUTH_USER_KEY, result.user);\n } else {\n // Invalid token - treat as unauthenticated\n c.set(AUTH_USER_KEY, null);\n }\n\n return next();\n },\n );\n}\n\n/**\n * Middleware to require authentication.\n * Returns 401 if user is not authenticated.\n *\n * @example\n * ```typescript\n * // Protect a single route\n * app.get('/protected', requireAuth(), (c) => {\n * const user = c.get('user')!; // User is guaranteed to exist\n * return c.json({ message: `Hello ${user.username}` });\n * });\n *\n * // Protect a group of routes\n * const protected = app.basePath('/api/admin');\n * protected.use('*', requireAuth());\n * ```\n */\nexport function requireAuth<TUser extends BackendUser = BackendUser>() {\n return createMiddleware<{ Variables: AuthVariables<TUser> }>(\n async (c, next) => {\n const user = c.get(AUTH_USER_KEY);\n\n if (!user) {\n return c.json({ error: \"Unauthorized\" }, 401);\n }\n\n return next();\n },\n );\n}\n\n/**\n * Helper to get the authenticated user from context.\n * Returns null if not authenticated.\n */\nexport function getUser<TUser extends BackendUser = BackendUser>(\n c: Context,\n): TUser | null {\n return c.get(AUTH_USER_KEY) as TUser | null;\n}\n\n/**\n * Helper to get the auth controller from context.\n */\nexport function getAuthController<TUser extends BackendUser = BackendUser>(\n c: Context,\n): BackendAuthController<TUser> {\n return c.get(AUTH_CONTROLLER_KEY) as BackendAuthController<TUser>;\n}\n","import type {\n InferRequestBody,\n InferRequestParams,\n InferResponseBody,\n RequestSchema,\n} from \"@nubase/core\";\nimport type { Context } from \"hono\";\nimport type { ContentfulStatusCode } from \"hono/utils/http-status\";\nimport { AUTH_USER_KEY } from \"./auth/middleware\";\nimport type { AuthLevel, BackendUser } from \"./auth/types\";\n\n/**\n * Custom HTTP error class that allows handlers to throw errors with specific status codes.\n * Use this to return proper HTTP error responses instead of generic 500 errors.\n *\n * @example\n * throw new HttpError(401, \"Invalid username or password\");\n * throw new HttpError(404, \"Resource not found\");\n * throw new HttpError(403, \"Access denied\");\n */\nexport class HttpError extends Error {\n constructor(\n public statusCode: ContentfulStatusCode,\n message: string,\n ) {\n super(message);\n this.name = \"HttpError\";\n }\n}\n\n/**\n * URL Parameter Coercion System (Backend)\n *\n * **The Problem:**\n * URL path parameters always arrive as strings from HTTP requests,\n * but schemas expect typed values (numbers, booleans).\n *\n * **The Solution:**\n * We use the schema's `toZodWithCoercion()` method which leverages Zod's\n * built-in coercion to automatically convert string values to expected types.\n *\n * **Example:**\n * - URL: `/tickets/37` → params: { id: \"37\" }\n * - Schema expects: { id: number }\n * - toZodWithCoercion() converts: { id: 37 }\n */\n\n/**\n * Context provided to typed handlers.\n *\n * @template T - The request schema type\n * @template TUser - The user type (when auth is required/optional)\n */\nexport type TypedHandlerContext<\n T extends RequestSchema,\n TUser extends BackendUser | null = null,\n> = {\n params: InferRequestParams<T>;\n body: InferRequestBody<T>;\n ctx: Context;\n /**\n * The authenticated user.\n * - When auth is 'required': TUser (guaranteed to exist)\n * - When auth is 'optional': TUser | null\n * - When auth is 'none': not present (null)\n */\n user: TUser;\n};\n\n/**\n * Handler function type for typed HTTP handlers.\n */\nexport type TypedHandler<\n T extends RequestSchema,\n TUser extends BackendUser | null = null,\n> = (context: TypedHandlerContext<T, TUser>) => Promise<InferResponseBody<T>>;\n\n// Overloaded function signatures for better ergonomics\nexport function createTypedHandler<T extends RequestSchema>(\n schema: T,\n handler: TypedHandler<T>,\n): ReturnType<typeof createTypedHandlerInternal>;\n\nexport function createTypedHandler<T extends RequestSchema>(\n endpointRef: T, // Can be apiEndpoints.ticketsGetTickets\n handler: TypedHandler<T>,\n): ReturnType<typeof createTypedHandlerInternal>;\n\nexport function createTypedHandler<T extends RequestSchema>(\n schemaOrEndpoint: T,\n handler: TypedHandler<T>,\n) {\n return createTypedHandlerInternal(schemaOrEndpoint, handler);\n}\n\n// Internal implementation\nfunction createTypedHandlerInternal<\n T extends RequestSchema,\n TUser extends BackendUser | null = null,\n>(schema: T, handler: TypedHandler<T, TUser>, options?: { auth?: AuthLevel }) {\n const authLevel = options?.auth ?? \"none\";\n\n return async (c: Context) => {\n try {\n // Check authentication if required\n const user = c.get(AUTH_USER_KEY) as TUser | null;\n\n if (authLevel === \"required\" && !user) {\n return c.json({ error: \"Unauthorized\" }, 401);\n }\n\n // Parse and validate request parameters using schema's built-in coercion\n let params: InferRequestParams<T>;\n try {\n const rawParams = c.req.param();\n\n // Use toZodWithCoercion() to automatically convert string params to expected types\n params = schema.requestParams\n .toZodWithCoercion()\n .parse(rawParams) as InferRequestParams<T>;\n } catch (error) {\n return c.json(\n {\n error: \"Invalid request parameters\",\n details: error instanceof Error ? error.message : String(error),\n },\n 400,\n );\n }\n\n // Parse and validate request body\n let body: InferRequestBody<T>;\n try {\n if (schema.requestBody) {\n // Only parse body if schema defines one\n const rawBody = schema.method === \"GET\" ? {} : await c.req.json();\n body = schema.requestBody\n .toZod()\n .parse(rawBody) as InferRequestBody<T>;\n } else {\n // No request body expected\n body = undefined as InferRequestBody<T>;\n }\n } catch (error) {\n return c.json(\n {\n error: \"Invalid request body\",\n details: error instanceof Error ? error.message : String(error),\n },\n 400,\n );\n }\n\n // Call the handler with typed context\n // For 'required' auth, user is guaranteed to be non-null\n // For 'optional' auth, user may be null\n // For 'none' auth, user is null\n const result = await handler({\n params,\n body,\n ctx: c,\n user: (authLevel === \"required\" ? user : (user ?? null)) as TUser,\n });\n\n // Validate response body (optional, for development safety)\n // Use passthrough() to preserve unknown properties in the response\n // This is important for dynamic data like chart series values (e.g., { category: \"Jan\", desktop: 186, mobile: 80 })\n try {\n const responseZod = schema.responseBody.toZod();\n // Apply passthrough if it's an object schema to preserve dynamic fields\n const passthroughZod =\n \"passthrough\" in responseZod\n ? responseZod.passthrough()\n : responseZod;\n const validatedResult = passthroughZod.parse(result);\n\n // Return appropriate status code based on method\n const statusCode = schema.method === \"POST\" ? 201 : 200;\n return c.json(validatedResult, statusCode);\n } catch (error) {\n console.error(\"Response validation failed:\", error);\n // In production, you might want to return the result anyway\n // For development, this helps catch schema mismatches\n return c.json(\n {\n error: \"Internal server error\",\n details: \"Response validation failed\",\n },\n 500,\n );\n }\n } catch (error) {\n // Check if it's an HttpError with a specific status code\n if (error instanceof HttpError) {\n return c.json(\n {\n error: error.message,\n },\n error.statusCode,\n );\n }\n\n console.error(\"Handler error:\", error);\n return c.json(\n {\n error: \"Internal server error\",\n details: error instanceof Error ? error.message : String(error),\n },\n 500,\n );\n }\n };\n}\n\nexport type TypedRouteDefinition<T extends RequestSchema> = {\n schema: T;\n handler: TypedHandler<T>;\n};\n\nexport type TypedRoutes = Record<string, TypedRouteDefinition<any>>;\n\nexport function createTypedRoutes<T extends TypedRoutes>(routes: T) {\n const honoHandlers: Record<\n string,\n ReturnType<typeof createTypedHandler>\n > = {};\n\n for (const [routeName, { schema, handler }] of Object.entries(routes)) {\n honoHandlers[routeName] = createTypedHandler(schema, handler);\n }\n\n return honoHandlers;\n}\n\n/**\n * A handler created by createHttpHandler with endpoint metadata attached.\n * This allows registerHandlers to auto-register routes based on the endpoint's path and method.\n */\nexport type HttpHandler = ((c: Context) => Promise<Response>) & {\n __endpoint: RequestSchema;\n};\n\n/**\n * Options for createHttpHandler.\n */\nexport type CreateHttpHandlerOptions<\n T extends RequestSchema,\n TAuth extends AuthLevel = \"none\",\n TUser extends BackendUser = BackendUser,\n> = {\n /**\n * The endpoint schema defining request/response types.\n */\n endpoint: T;\n\n /**\n * Authentication level for this route.\n * - 'required': Request must be authenticated. Returns 401 if not. User is guaranteed in handler.\n * - 'optional': Authentication is optional. User may be null in handler.\n * - 'none': No authentication check. User is always null. (default)\n */\n auth?: TAuth;\n\n /**\n * The handler function.\n * When auth is 'required', user is guaranteed to be non-null.\n * When auth is 'optional', user may be null.\n * When auth is 'none', user is null.\n */\n handler: TypedHandler<\n T,\n TAuth extends \"required\"\n ? TUser\n : TAuth extends \"optional\"\n ? TUser | null\n : null\n >;\n};\n\n/**\n * Create a typed HTTP handler with optional authentication.\n *\n * @example\n * ```typescript\n * // No auth (default)\n * export const handleGetPublicData = createHttpHandler({\n * endpoint: apiEndpoints.getPublicData,\n * handler: async ({ body }) => {\n * return { data: 'public' };\n * },\n * });\n *\n * // Required auth - user is guaranteed\n * export const handleGetProfile = createHttpHandler({\n * endpoint: apiEndpoints.getProfile,\n * auth: 'required',\n * handler: async ({ body, user }) => {\n * // user is guaranteed to exist here\n * return { userId: user.id };\n * },\n * });\n *\n * // Optional auth - user may be null\n * export const handleGetContent = createHttpHandler({\n * endpoint: apiEndpoints.getContent,\n * auth: 'optional',\n * handler: async ({ body, user }) => {\n * if (user) {\n * return { content: 'personalized', userId: user.id };\n * }\n * return { content: 'generic' };\n * },\n * });\n * ```\n */\nexport function createHttpHandler<\n T extends RequestSchema,\n TAuth extends AuthLevel = \"none\",\n TUser extends BackendUser = BackendUser,\n>(options: CreateHttpHandlerOptions<T, TAuth, TUser>): HttpHandler {\n const { endpoint, handler, auth } = options;\n const honoHandler = createTypedHandlerInternal(\n endpoint,\n handler as TypedHandler<T, any>,\n {\n auth,\n },\n );\n\n // Attach endpoint metadata to the handler for auto-registration\n return Object.assign(honoHandler, { __endpoint: endpoint }) as HttpHandler;\n}\n\n/**\n * A record of handlers to be registered with registerHandlers.\n */\nexport type HttpHandlers = Record<string, HttpHandler>;\n\n/**\n * Register multiple HTTP handlers with a Hono app.\n * Automatically extracts path and method from each handler's endpoint metadata.\n *\n * @example\n * ```typescript\n * // In dashboard.ts\n * export const dashboardHandlers = {\n * getRevenueChart: createHttpHandler({\n * endpoint: apiEndpoints.getRevenueChart,\n * auth: \"required\",\n * handler: async () => ({ ... }),\n * }),\n * getBrowserStats: createHttpHandler({\n * endpoint: apiEndpoints.getBrowserStats,\n * auth: \"required\",\n * handler: async () => ({ ... }),\n * }),\n * };\n *\n * // In index.ts\n * registerHandlers(app, dashboardHandlers);\n * // Automatically registers:\n * // app.get(\"/dashboard/revenue-chart\", handler)\n * // app.get(\"/dashboard/browser-stats\", handler)\n * ```\n */\nexport function registerHandlers<\n TApp extends { get: any; post: any; put: any; patch: any; delete: any },\n>(app: TApp, handlers: HttpHandlers): void {\n for (const handler of Object.values(handlers)) {\n const { method, path } = handler.__endpoint;\n const methodLower = method.toLowerCase() as\n | \"get\"\n | \"post\"\n | \"put\"\n | \"patch\"\n | \"delete\";\n app[methodLower](path, handler);\n }\n}\n","/**\n * Parse a cookie header string into a key-value object.\n *\n * @param cookieHeader - The Cookie header string (e.g., \"name=value; other=123\")\n * @returns An object mapping cookie names to their values\n *\n * @example\n * ```typescript\n * const cookies = parseCookies(\"session=abc123; theme=dark\");\n * // { session: \"abc123\", theme: \"dark\" }\n * ```\n */\nexport function parseCookies(cookieHeader: string): Record<string, string> {\n const cookies: Record<string, string> = {};\n\n if (!cookieHeader) {\n return cookies;\n }\n\n for (const cookie of cookieHeader.split(\";\")) {\n const [name, ...rest] = cookie.split(\"=\");\n if (name) {\n cookies[name.trim()] = rest.join(\"=\").trim();\n }\n }\n\n return cookies;\n}\n\n/**\n * Get a specific cookie value from a cookie header string.\n *\n * @param cookieHeader - The Cookie header string\n * @param name - The cookie name to retrieve\n * @returns The cookie value or null if not found\n *\n * @example\n * ```typescript\n * const session = getCookie(\"session=abc123; theme=dark\", \"session\");\n * // \"abc123\"\n * ```\n */\nexport function getCookie(cookieHeader: string, name: string): string | null {\n const cookies = parseCookies(cookieHeader);\n return cookies[name] ?? null;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/auth/handlers.ts","../src/auth/middleware.ts","../src/typed-handlers.ts","../src/utils/cookies.ts","../src/utils/validate-environment.ts"],"sourcesContent":["export * from \"./auth\";\nexport * from \"./typed-handlers\";\nexport * from \"./utils\";\n","import type { Context } from \"hono\";\nimport { Hono } from \"hono\";\nimport { AUTH_USER_KEY } from \"./middleware\";\nimport type { BackendAuthController, BackendUser } from \"./types\";\n\n/**\n * Options for creating auth handlers.\n */\nexport interface CreateAuthHandlersOptions<\n TUser extends BackendUser = BackendUser,\n> {\n /**\n * The auth controller instance.\n */\n controller: BackendAuthController<TUser>;\n}\n\n/**\n * Auth handlers returned by createAuthHandlers.\n */\nexport interface AuthHandlers {\n /**\n * Login handler - validates credentials and sets auth cookie.\n * Expects JSON body: { username: string, password: string }\n * Returns: { user: { id, email, username } }\n */\n login: (ctx: Context) => Promise<Response>;\n\n /**\n * Logout handler - clears the auth cookie.\n * Returns: { success: true }\n */\n logout: (ctx: Context) => Promise<Response>;\n\n /**\n * Get current user handler - returns the authenticated user or undefined.\n * Returns: { user?: { id, email, username } }\n */\n getMe: (ctx: Context) => Promise<Response>;\n\n /**\n * Pre-configured Hono router with all auth routes.\n * Mount this at /auth to get /auth/login, /auth/logout, /auth/me\n */\n routes: Hono;\n}\n\n/**\n * Create standard authentication handlers from a BackendAuthController.\n *\n * This utility reduces boilerplate by providing pre-built handlers for\n * login, logout, and get-current-user endpoints.\n *\n * @example\n * ```typescript\n * import { createAuthHandlers, createAuthMiddleware } from \"@nubase/backend\";\n *\n * const authController = new MyBackendAuthController();\n * const authHandlers = createAuthHandlers({ controller: authController });\n *\n * const app = new Hono();\n * app.use(\"*\", createAuthMiddleware({ controller: authController }));\n *\n * // Option 1: Register routes individually\n * app.post(\"/auth/login\", authHandlers.login);\n * app.post(\"/auth/logout\", authHandlers.logout);\n * app.get(\"/auth/me\", authHandlers.getMe);\n *\n * // Option 2: Mount the pre-configured router\n * app.route(\"/auth\", authHandlers.routes);\n * ```\n */\nexport function createAuthHandlers<TUser extends BackendUser = BackendUser>(\n options: CreateAuthHandlersOptions<TUser>,\n): AuthHandlers {\n const { controller } = options;\n\n const login = async (ctx: Context): Promise<Response> => {\n try {\n const body = await ctx.req.json<{ username: string; password: string }>();\n\n if (!body.username || !body.password) {\n return ctx.json({ error: \"Username and password are required\" }, 400);\n }\n\n const user = await controller.validateCredentials(\n body.username,\n body.password,\n );\n\n if (!user) {\n return ctx.json({ error: \"Invalid username or password\" }, 401);\n }\n\n const token = await controller.createToken(user);\n controller.setTokenInResponse(ctx, token);\n\n return ctx.json({ user }, 201);\n } catch (error) {\n console.error(\"Login error:\", error);\n return ctx.json({ error: \"Login failed\" }, 500);\n }\n };\n\n const logout = async (ctx: Context): Promise<Response> => {\n controller.clearTokenFromResponse(ctx);\n return ctx.json({ success: true }, 200);\n };\n\n const getMe = async (ctx: Context): Promise<Response> => {\n const user = ctx.get(AUTH_USER_KEY) as TUser | null;\n\n if (!user) {\n return ctx.json({ user: undefined }, 200);\n }\n\n return ctx.json({ user }, 200);\n };\n\n // Create pre-configured router\n const routes = new Hono();\n routes.post(\"/login\", login);\n routes.post(\"/logout\", logout);\n routes.get(\"/me\", getMe);\n\n return {\n login,\n logout,\n getMe,\n routes,\n };\n}\n","import type { Context } from \"hono\";\nimport { createMiddleware } from \"hono/factory\";\nimport type { AuthLevel, BackendAuthController, BackendUser } from \"./types\";\n\n/**\n * Context key for storing the authenticated user.\n * Use `c.get('user')` to retrieve the user in handlers.\n */\nexport const AUTH_USER_KEY = \"user\";\n\n/**\n * Context key for storing the auth controller instance.\n */\nexport const AUTH_CONTROLLER_KEY = \"authController\";\n\n/**\n * Variables added to Hono context by auth middleware.\n */\nexport interface AuthVariables<TUser extends BackendUser = BackendUser> {\n user: TUser | null;\n authController: BackendAuthController<TUser>;\n}\n\n/**\n * Options for the auth middleware.\n */\nexport interface AuthMiddlewareOptions<\n TUser extends BackendUser = BackendUser,\n> {\n /**\n * The auth controller instance to use for token verification.\n */\n controller: BackendAuthController<TUser>;\n\n /**\n * Default auth level for all routes.\n * Can be overridden per-route using createHttpHandler's auth option.\n * @default \"none\"\n */\n defaultAuthLevel?: AuthLevel;\n}\n\n/**\n * Create an authentication middleware for Hono.\n *\n * This middleware:\n * 1. Extracts the token from the request\n * 2. Verifies the token using the provided controller\n * 3. Sets the user in the context (or null if not authenticated)\n * 4. Makes the controller available in context for handlers\n *\n * @example\n * ```typescript\n * const authController = new QuestlogBackendAuthController();\n * const app = new Hono();\n *\n * // Apply to all routes\n * app.use('*', createAuthMiddleware({ controller: authController }));\n *\n * // Access user in handlers\n * app.get('/me', (c) => {\n * const user = c.get('user');\n * if (!user) return c.json({ error: 'Unauthorized' }, 401);\n * return c.json({ user });\n * });\n * ```\n */\nexport function createAuthMiddleware<TUser extends BackendUser = BackendUser>(\n options: AuthMiddlewareOptions<TUser>,\n) {\n const { controller } = options;\n\n return createMiddleware<{ Variables: AuthVariables<TUser> }>(\n async (c, next) => {\n // Store the controller in context for use by handlers\n c.set(AUTH_CONTROLLER_KEY, controller);\n\n // Extract token from request\n const token = controller.extractToken(c);\n\n // Debug logging\n const cookieHeader = c.req.header(\"Cookie\");\n console.info(\n `[Auth] ${c.req.method} ${c.req.path} - Cookie header: ${cookieHeader ? `${cookieHeader.substring(0, 50)}...` : \"(none)\"}`,\n );\n console.info(`[Auth] Token extracted: ${token ? \"yes\" : \"no\"}`);\n\n if (!token) {\n // No token present - user is not authenticated\n c.set(AUTH_USER_KEY, null);\n return next();\n }\n\n // Verify the token\n const result = await controller.verifyToken(token);\n\n console.info(\n `[Auth] Token verification: ${result.valid ? \"valid\" : `invalid - ${result.error}`}`,\n );\n\n if (result.valid) {\n c.set(AUTH_USER_KEY, result.user);\n } else {\n // Invalid token - treat as unauthenticated\n c.set(AUTH_USER_KEY, null);\n }\n\n return next();\n },\n );\n}\n\n/**\n * Middleware to require authentication.\n * Returns 401 if user is not authenticated.\n *\n * @example\n * ```typescript\n * // Protect a single route\n * app.get('/protected', requireAuth(), (c) => {\n * const user = c.get('user')!; // User is guaranteed to exist\n * return c.json({ message: `Hello ${user.username}` });\n * });\n *\n * // Protect a group of routes\n * const protected = app.basePath('/api/admin');\n * protected.use('*', requireAuth());\n * ```\n */\nexport function requireAuth<TUser extends BackendUser = BackendUser>() {\n return createMiddleware<{ Variables: AuthVariables<TUser> }>(\n async (c, next) => {\n const user = c.get(AUTH_USER_KEY);\n\n if (!user) {\n return c.json({ error: \"Unauthorized\" }, 401);\n }\n\n return next();\n },\n );\n}\n\n/**\n * Helper to get the authenticated user from context.\n * Returns null if not authenticated.\n */\nexport function getUser<TUser extends BackendUser = BackendUser>(\n c: Context,\n): TUser | null {\n return c.get(AUTH_USER_KEY) as TUser | null;\n}\n\n/**\n * Helper to get the auth controller from context.\n */\nexport function getAuthController<TUser extends BackendUser = BackendUser>(\n c: Context,\n): BackendAuthController<TUser> {\n return c.get(AUTH_CONTROLLER_KEY) as BackendAuthController<TUser>;\n}\n","import type {\n InferRequestBody,\n InferRequestParams,\n InferResponseBody,\n RequestSchema,\n} from \"@nubase/core\";\nimport type { Context } from \"hono\";\nimport type { ContentfulStatusCode } from \"hono/utils/http-status\";\nimport { AUTH_USER_KEY } from \"./auth/middleware\";\nimport type { AuthLevel, BackendUser } from \"./auth/types\";\n\n/**\n * Custom HTTP error class that allows handlers to throw errors with specific status codes.\n * Use this to return proper HTTP error responses instead of generic 500 errors.\n *\n * @example\n * throw new HttpError(401, \"Invalid username or password\");\n * throw new HttpError(404, \"Resource not found\");\n * throw new HttpError(403, \"Access denied\");\n */\nexport class HttpError extends Error {\n constructor(\n public statusCode: ContentfulStatusCode,\n message: string,\n ) {\n super(message);\n this.name = \"HttpError\";\n }\n}\n\n/**\n * URL Parameter Coercion System (Backend)\n *\n * **The Problem:**\n * URL path parameters always arrive as strings from HTTP requests,\n * but schemas expect typed values (numbers, booleans).\n *\n * **The Solution:**\n * We use the schema's `toZodWithCoercion()` method which leverages Zod's\n * built-in coercion to automatically convert string values to expected types.\n *\n * **Example:**\n * - URL: `/tickets/37` → params: { id: \"37\" }\n * - Schema expects: { id: number }\n * - toZodWithCoercion() converts: { id: 37 }\n */\n\n/**\n * Context provided to typed handlers.\n *\n * @template T - The request schema type\n * @template TUser - The user type (when auth is required/optional)\n */\nexport type TypedHandlerContext<\n T extends RequestSchema,\n TUser extends BackendUser | null = null,\n> = {\n params: InferRequestParams<T>;\n body: InferRequestBody<T>;\n ctx: Context;\n /**\n * The authenticated user.\n * - When auth is 'required': TUser (guaranteed to exist)\n * - When auth is 'optional': TUser | null\n * - When auth is 'none': not present (null)\n */\n user: TUser;\n};\n\n/**\n * Handler function type for typed HTTP handlers.\n */\nexport type TypedHandler<\n T extends RequestSchema,\n TUser extends BackendUser | null = null,\n> = (context: TypedHandlerContext<T, TUser>) => Promise<InferResponseBody<T>>;\n\n// Overloaded function signatures for better ergonomics\nexport function createTypedHandler<T extends RequestSchema>(\n schema: T,\n handler: TypedHandler<T>,\n): ReturnType<typeof createTypedHandlerInternal>;\n\nexport function createTypedHandler<T extends RequestSchema>(\n endpointRef: T, // Can be apiEndpoints.ticketsGetTickets\n handler: TypedHandler<T>,\n): ReturnType<typeof createTypedHandlerInternal>;\n\nexport function createTypedHandler<T extends RequestSchema>(\n schemaOrEndpoint: T,\n handler: TypedHandler<T>,\n) {\n return createTypedHandlerInternal(schemaOrEndpoint, handler);\n}\n\n// Internal implementation\nfunction createTypedHandlerInternal<\n T extends RequestSchema,\n TUser extends BackendUser | null = null,\n>(schema: T, handler: TypedHandler<T, TUser>, options?: { auth?: AuthLevel }) {\n const authLevel = options?.auth ?? \"none\";\n\n return async (c: Context) => {\n try {\n // Check authentication if required\n const user = c.get(AUTH_USER_KEY) as TUser | null;\n\n if (authLevel === \"required\" && !user) {\n return c.json({ error: \"Unauthorized\" }, 401);\n }\n\n // Parse and validate request parameters using schema's built-in coercion\n let params: InferRequestParams<T>;\n try {\n // Merge path params (e.g., :id from /tickets/:id) and query params (e.g., ?q=admin)\n const pathParams = c.req.param();\n const queryParams = c.req.query();\n const rawParams = { ...pathParams, ...queryParams };\n\n // Use toZodWithCoercion() to automatically convert string params to expected types\n params = schema.requestParams\n .toZodWithCoercion()\n .parse(rawParams) as InferRequestParams<T>;\n } catch (error) {\n return c.json(\n {\n error: \"Invalid request parameters\",\n details: error instanceof Error ? error.message : String(error),\n },\n 400,\n );\n }\n\n // Parse and validate request body\n let body: InferRequestBody<T>;\n try {\n if (schema.requestBody) {\n // Only parse body if schema defines one\n const rawBody = schema.method === \"GET\" ? {} : await c.req.json();\n body = schema.requestBody\n .toZod()\n .parse(rawBody) as InferRequestBody<T>;\n } else {\n // No request body expected\n body = undefined as InferRequestBody<T>;\n }\n } catch (error) {\n return c.json(\n {\n error: \"Invalid request body\",\n details: error instanceof Error ? error.message : String(error),\n },\n 400,\n );\n }\n\n // Call the handler with typed context\n // For 'required' auth, user is guaranteed to be non-null\n // For 'optional' auth, user may be null\n // For 'none' auth, user is null\n const result = await handler({\n params,\n body,\n ctx: c,\n user: (authLevel === \"required\" ? user : (user ?? null)) as TUser,\n });\n\n // Validate response body (optional, for development safety)\n // Use passthrough() to preserve unknown properties in the response\n // This is important for dynamic data like chart series values (e.g., { category: \"Jan\", desktop: 186, mobile: 80 })\n try {\n const responseZod = schema.responseBody.toZod();\n // Apply passthrough if it's an object schema to preserve dynamic fields\n const passthroughZod =\n \"passthrough\" in responseZod\n ? responseZod.passthrough()\n : responseZod;\n const validatedResult = passthroughZod.parse(result);\n\n // Return appropriate status code based on method\n const statusCode = schema.method === \"POST\" ? 201 : 200;\n return c.json(validatedResult, statusCode);\n } catch (error) {\n console.error(\"Response validation failed:\", error);\n // In production, you might want to return the result anyway\n // For development, this helps catch schema mismatches\n return c.json(\n {\n error: \"Internal server error\",\n details: \"Response validation failed\",\n },\n 500,\n );\n }\n } catch (error) {\n // Check if it's an HttpError with a specific status code\n if (error instanceof HttpError) {\n return c.json(\n {\n error: error.message,\n },\n error.statusCode,\n );\n }\n\n console.error(\"Handler error:\", error);\n return c.json(\n {\n error: \"Internal server error\",\n details: error instanceof Error ? error.message : String(error),\n },\n 500,\n );\n }\n };\n}\n\nexport type TypedRouteDefinition<T extends RequestSchema> = {\n schema: T;\n handler: TypedHandler<T>;\n};\n\nexport type TypedRoutes = Record<string, TypedRouteDefinition<any>>;\n\nexport function createTypedRoutes<T extends TypedRoutes>(routes: T) {\n const honoHandlers: Record<\n string,\n ReturnType<typeof createTypedHandler>\n > = {};\n\n for (const [routeName, { schema, handler }] of Object.entries(routes)) {\n honoHandlers[routeName] = createTypedHandler(schema, handler);\n }\n\n return honoHandlers;\n}\n\n/**\n * A handler created by createHttpHandler with endpoint metadata attached.\n * This allows registerHandlers to auto-register routes based on the endpoint's path and method.\n */\nexport type HttpHandler = ((c: Context) => Promise<Response>) & {\n __endpoint: RequestSchema;\n};\n\n/**\n * Options for createHttpHandler.\n */\nexport type CreateHttpHandlerOptions<\n T extends RequestSchema,\n TAuth extends AuthLevel = \"none\",\n TUser extends BackendUser = BackendUser,\n> = {\n /**\n * The endpoint schema defining request/response types.\n */\n endpoint: T;\n\n /**\n * Authentication level for this route.\n * - 'required': Request must be authenticated. Returns 401 if not. User is guaranteed in handler.\n * - 'optional': Authentication is optional. User may be null in handler.\n * - 'none': No authentication check. User is always null. (default)\n */\n auth?: TAuth;\n\n /**\n * The handler function.\n * When auth is 'required', user is guaranteed to be non-null.\n * When auth is 'optional', user may be null.\n * When auth is 'none', user is null.\n */\n handler: TypedHandler<\n T,\n TAuth extends \"required\"\n ? TUser\n : TAuth extends \"optional\"\n ? TUser | null\n : null\n >;\n};\n\n/**\n * Create a typed HTTP handler with optional authentication.\n *\n * @example\n * ```typescript\n * // No auth (default)\n * export const handleGetPublicData = createHttpHandler({\n * endpoint: apiEndpoints.getPublicData,\n * handler: async ({ body }) => {\n * return { data: 'public' };\n * },\n * });\n *\n * // Required auth - user is guaranteed\n * export const handleGetProfile = createHttpHandler({\n * endpoint: apiEndpoints.getProfile,\n * auth: 'required',\n * handler: async ({ body, user }) => {\n * // user is guaranteed to exist here\n * return { userId: user.id };\n * },\n * });\n *\n * // Optional auth - user may be null\n * export const handleGetContent = createHttpHandler({\n * endpoint: apiEndpoints.getContent,\n * auth: 'optional',\n * handler: async ({ body, user }) => {\n * if (user) {\n * return { content: 'personalized', userId: user.id };\n * }\n * return { content: 'generic' };\n * },\n * });\n * ```\n */\nexport function createHttpHandler<\n T extends RequestSchema,\n TAuth extends AuthLevel = \"none\",\n TUser extends BackendUser = BackendUser,\n>(options: CreateHttpHandlerOptions<T, TAuth, TUser>): HttpHandler {\n const { endpoint, handler, auth } = options;\n const honoHandler = createTypedHandlerInternal(\n endpoint,\n handler as TypedHandler<T, any>,\n {\n auth,\n },\n );\n\n // Attach endpoint metadata to the handler for auto-registration\n return Object.assign(honoHandler, { __endpoint: endpoint }) as HttpHandler;\n}\n\n/**\n * A record of handlers to be registered with registerHandlers.\n */\nexport type HttpHandlers = Record<string, HttpHandler>;\n\n/**\n * Register multiple HTTP handlers with a Hono app.\n * Automatically extracts path and method from each handler's endpoint metadata.\n *\n * @example\n * ```typescript\n * // In dashboard.ts\n * export const dashboardHandlers = {\n * getRevenueChart: createHttpHandler({\n * endpoint: apiEndpoints.getRevenueChart,\n * auth: \"required\",\n * handler: async () => ({ ... }),\n * }),\n * getBrowserStats: createHttpHandler({\n * endpoint: apiEndpoints.getBrowserStats,\n * auth: \"required\",\n * handler: async () => ({ ... }),\n * }),\n * };\n *\n * // In index.ts\n * registerHandlers(app, dashboardHandlers);\n * // Automatically registers:\n * // app.get(\"/dashboard/revenue-chart\", handler)\n * // app.get(\"/dashboard/browser-stats\", handler)\n * ```\n */\nexport function registerHandlers<\n TApp extends { get: any; post: any; put: any; patch: any; delete: any },\n>(app: TApp, handlers: HttpHandlers): void {\n for (const handler of Object.values(handlers)) {\n const { method, path } = handler.__endpoint;\n const methodLower = method.toLowerCase() as\n | \"get\"\n | \"post\"\n | \"put\"\n | \"patch\"\n | \"delete\";\n app[methodLower](path, handler);\n }\n}\n","/**\n * Parse a cookie header string into a key-value object.\n *\n * @param cookieHeader - The Cookie header string (e.g., \"name=value; other=123\")\n * @returns An object mapping cookie names to their values\n *\n * @example\n * ```typescript\n * const cookies = parseCookies(\"session=abc123; theme=dark\");\n * // { session: \"abc123\", theme: \"dark\" }\n * ```\n */\nexport function parseCookies(cookieHeader: string): Record<string, string> {\n const cookies: Record<string, string> = {};\n\n if (!cookieHeader) {\n return cookies;\n }\n\n for (const cookie of cookieHeader.split(\";\")) {\n const [name, ...rest] = cookie.split(\"=\");\n if (name) {\n cookies[name.trim()] = rest.join(\"=\").trim();\n }\n }\n\n return cookies;\n}\n\n/**\n * Get a specific cookie value from a cookie header string.\n *\n * @param cookieHeader - The Cookie header string\n * @param name - The cookie name to retrieve\n * @returns The cookie value or null if not found\n *\n * @example\n * ```typescript\n * const session = getCookie(\"session=abc123; theme=dark\", \"session\");\n * // \"abc123\"\n * ```\n */\nexport function getCookie(cookieHeader: string, name: string): string | null {\n const cookies = parseCookies(cookieHeader);\n return cookies[name] ?? null;\n}\n","import { Client } from \"pg\";\n\nexport interface ValidateEnvironmentOptions {\n /**\n * The database URL to validate connectivity against.\n * Defaults to process.env.DATABASE_URL\n */\n databaseUrl?: string;\n}\n\n/**\n * Validates that the environment is properly configured and database is accessible.\n * Should be called at application startup before starting the server.\n *\n * @throws Error if DATABASE_URL is not defined or database is not accessible\n */\nexport async function validateEnvironment(\n options: ValidateEnvironmentOptions = {},\n): Promise<void> {\n const databaseUrl = options.databaseUrl ?? process.env.DATABASE_URL;\n\n if (!databaseUrl) {\n throw new Error(\n \"DATABASE_URL is not defined in the environment variables.\",\n );\n }\n\n const client = new Client({ connectionString: databaseUrl });\n\n try {\n await client.connect();\n await client.query(\"SELECT 1\");\n await client.end();\n } catch (err) {\n await client.end().catch(() => {});\n throw new Error(\n `Failed to connect to database: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.mjs
CHANGED
|
@@ -119,7 +119,9 @@ function createTypedHandlerInternal(schema, handler, options) {
|
|
|
119
119
|
}
|
|
120
120
|
let params;
|
|
121
121
|
try {
|
|
122
|
-
const
|
|
122
|
+
const pathParams = c.req.param();
|
|
123
|
+
const queryParams = c.req.query();
|
|
124
|
+
const rawParams = { ...pathParams, ...queryParams };
|
|
123
125
|
params = schema.requestParams.toZodWithCoercion().parse(rawParams);
|
|
124
126
|
} catch (error) {
|
|
125
127
|
return c.json(
|
|
@@ -233,6 +235,29 @@ function getCookie(cookieHeader, name) {
|
|
|
233
235
|
const cookies = parseCookies(cookieHeader);
|
|
234
236
|
return cookies[name] ?? null;
|
|
235
237
|
}
|
|
238
|
+
|
|
239
|
+
// src/utils/validate-environment.ts
|
|
240
|
+
import { Client } from "pg";
|
|
241
|
+
async function validateEnvironment(options = {}) {
|
|
242
|
+
const databaseUrl = options.databaseUrl ?? process.env.DATABASE_URL;
|
|
243
|
+
if (!databaseUrl) {
|
|
244
|
+
throw new Error(
|
|
245
|
+
"DATABASE_URL is not defined in the environment variables."
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
const client = new Client({ connectionString: databaseUrl });
|
|
249
|
+
try {
|
|
250
|
+
await client.connect();
|
|
251
|
+
await client.query("SELECT 1");
|
|
252
|
+
await client.end();
|
|
253
|
+
} catch (err) {
|
|
254
|
+
await client.end().catch(() => {
|
|
255
|
+
});
|
|
256
|
+
throw new Error(
|
|
257
|
+
`Failed to connect to database: ${err instanceof Error ? err.message : String(err)}`
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
236
261
|
export {
|
|
237
262
|
AUTH_CONTROLLER_KEY,
|
|
238
263
|
AUTH_USER_KEY,
|
|
@@ -247,6 +272,7 @@ export {
|
|
|
247
272
|
getUser,
|
|
248
273
|
parseCookies,
|
|
249
274
|
registerHandlers,
|
|
250
|
-
requireAuth
|
|
275
|
+
requireAuth,
|
|
276
|
+
validateEnvironment
|
|
251
277
|
};
|
|
252
278
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/auth/handlers.ts","../src/auth/middleware.ts","../src/typed-handlers.ts","../src/utils/cookies.ts"],"sourcesContent":["import type { Context } from \"hono\";\nimport { Hono } from \"hono\";\nimport { AUTH_USER_KEY } from \"./middleware\";\nimport type { BackendAuthController, BackendUser } from \"./types\";\n\n/**\n * Options for creating auth handlers.\n */\nexport interface CreateAuthHandlersOptions<\n TUser extends BackendUser = BackendUser,\n> {\n /**\n * The auth controller instance.\n */\n controller: BackendAuthController<TUser>;\n}\n\n/**\n * Auth handlers returned by createAuthHandlers.\n */\nexport interface AuthHandlers {\n /**\n * Login handler - validates credentials and sets auth cookie.\n * Expects JSON body: { username: string, password: string }\n * Returns: { user: { id, email, username } }\n */\n login: (ctx: Context) => Promise<Response>;\n\n /**\n * Logout handler - clears the auth cookie.\n * Returns: { success: true }\n */\n logout: (ctx: Context) => Promise<Response>;\n\n /**\n * Get current user handler - returns the authenticated user or undefined.\n * Returns: { user?: { id, email, username } }\n */\n getMe: (ctx: Context) => Promise<Response>;\n\n /**\n * Pre-configured Hono router with all auth routes.\n * Mount this at /auth to get /auth/login, /auth/logout, /auth/me\n */\n routes: Hono;\n}\n\n/**\n * Create standard authentication handlers from a BackendAuthController.\n *\n * This utility reduces boilerplate by providing pre-built handlers for\n * login, logout, and get-current-user endpoints.\n *\n * @example\n * ```typescript\n * import { createAuthHandlers, createAuthMiddleware } from \"@nubase/backend\";\n *\n * const authController = new MyBackendAuthController();\n * const authHandlers = createAuthHandlers({ controller: authController });\n *\n * const app = new Hono();\n * app.use(\"*\", createAuthMiddleware({ controller: authController }));\n *\n * // Option 1: Register routes individually\n * app.post(\"/auth/login\", authHandlers.login);\n * app.post(\"/auth/logout\", authHandlers.logout);\n * app.get(\"/auth/me\", authHandlers.getMe);\n *\n * // Option 2: Mount the pre-configured router\n * app.route(\"/auth\", authHandlers.routes);\n * ```\n */\nexport function createAuthHandlers<TUser extends BackendUser = BackendUser>(\n options: CreateAuthHandlersOptions<TUser>,\n): AuthHandlers {\n const { controller } = options;\n\n const login = async (ctx: Context): Promise<Response> => {\n try {\n const body = await ctx.req.json<{ username: string; password: string }>();\n\n if (!body.username || !body.password) {\n return ctx.json({ error: \"Username and password are required\" }, 400);\n }\n\n const user = await controller.validateCredentials(\n body.username,\n body.password,\n );\n\n if (!user) {\n return ctx.json({ error: \"Invalid username or password\" }, 401);\n }\n\n const token = await controller.createToken(user);\n controller.setTokenInResponse(ctx, token);\n\n return ctx.json({ user }, 201);\n } catch (error) {\n console.error(\"Login error:\", error);\n return ctx.json({ error: \"Login failed\" }, 500);\n }\n };\n\n const logout = async (ctx: Context): Promise<Response> => {\n controller.clearTokenFromResponse(ctx);\n return ctx.json({ success: true }, 200);\n };\n\n const getMe = async (ctx: Context): Promise<Response> => {\n const user = ctx.get(AUTH_USER_KEY) as TUser | null;\n\n if (!user) {\n return ctx.json({ user: undefined }, 200);\n }\n\n return ctx.json({ user }, 200);\n };\n\n // Create pre-configured router\n const routes = new Hono();\n routes.post(\"/login\", login);\n routes.post(\"/logout\", logout);\n routes.get(\"/me\", getMe);\n\n return {\n login,\n logout,\n getMe,\n routes,\n };\n}\n","import type { Context } from \"hono\";\nimport { createMiddleware } from \"hono/factory\";\nimport type { AuthLevel, BackendAuthController, BackendUser } from \"./types\";\n\n/**\n * Context key for storing the authenticated user.\n * Use `c.get('user')` to retrieve the user in handlers.\n */\nexport const AUTH_USER_KEY = \"user\";\n\n/**\n * Context key for storing the auth controller instance.\n */\nexport const AUTH_CONTROLLER_KEY = \"authController\";\n\n/**\n * Variables added to Hono context by auth middleware.\n */\nexport interface AuthVariables<TUser extends BackendUser = BackendUser> {\n user: TUser | null;\n authController: BackendAuthController<TUser>;\n}\n\n/**\n * Options for the auth middleware.\n */\nexport interface AuthMiddlewareOptions<\n TUser extends BackendUser = BackendUser,\n> {\n /**\n * The auth controller instance to use for token verification.\n */\n controller: BackendAuthController<TUser>;\n\n /**\n * Default auth level for all routes.\n * Can be overridden per-route using createHttpHandler's auth option.\n * @default \"none\"\n */\n defaultAuthLevel?: AuthLevel;\n}\n\n/**\n * Create an authentication middleware for Hono.\n *\n * This middleware:\n * 1. Extracts the token from the request\n * 2. Verifies the token using the provided controller\n * 3. Sets the user in the context (or null if not authenticated)\n * 4. Makes the controller available in context for handlers\n *\n * @example\n * ```typescript\n * const authController = new QuestlogBackendAuthController();\n * const app = new Hono();\n *\n * // Apply to all routes\n * app.use('*', createAuthMiddleware({ controller: authController }));\n *\n * // Access user in handlers\n * app.get('/me', (c) => {\n * const user = c.get('user');\n * if (!user) return c.json({ error: 'Unauthorized' }, 401);\n * return c.json({ user });\n * });\n * ```\n */\nexport function createAuthMiddleware<TUser extends BackendUser = BackendUser>(\n options: AuthMiddlewareOptions<TUser>,\n) {\n const { controller } = options;\n\n return createMiddleware<{ Variables: AuthVariables<TUser> }>(\n async (c, next) => {\n // Store the controller in context for use by handlers\n c.set(AUTH_CONTROLLER_KEY, controller);\n\n // Extract token from request\n const token = controller.extractToken(c);\n\n // Debug logging\n const cookieHeader = c.req.header(\"Cookie\");\n console.info(\n `[Auth] ${c.req.method} ${c.req.path} - Cookie header: ${cookieHeader ? `${cookieHeader.substring(0, 50)}...` : \"(none)\"}`,\n );\n console.info(`[Auth] Token extracted: ${token ? \"yes\" : \"no\"}`);\n\n if (!token) {\n // No token present - user is not authenticated\n c.set(AUTH_USER_KEY, null);\n return next();\n }\n\n // Verify the token\n const result = await controller.verifyToken(token);\n\n console.info(\n `[Auth] Token verification: ${result.valid ? \"valid\" : `invalid - ${result.error}`}`,\n );\n\n if (result.valid) {\n c.set(AUTH_USER_KEY, result.user);\n } else {\n // Invalid token - treat as unauthenticated\n c.set(AUTH_USER_KEY, null);\n }\n\n return next();\n },\n );\n}\n\n/**\n * Middleware to require authentication.\n * Returns 401 if user is not authenticated.\n *\n * @example\n * ```typescript\n * // Protect a single route\n * app.get('/protected', requireAuth(), (c) => {\n * const user = c.get('user')!; // User is guaranteed to exist\n * return c.json({ message: `Hello ${user.username}` });\n * });\n *\n * // Protect a group of routes\n * const protected = app.basePath('/api/admin');\n * protected.use('*', requireAuth());\n * ```\n */\nexport function requireAuth<TUser extends BackendUser = BackendUser>() {\n return createMiddleware<{ Variables: AuthVariables<TUser> }>(\n async (c, next) => {\n const user = c.get(AUTH_USER_KEY);\n\n if (!user) {\n return c.json({ error: \"Unauthorized\" }, 401);\n }\n\n return next();\n },\n );\n}\n\n/**\n * Helper to get the authenticated user from context.\n * Returns null if not authenticated.\n */\nexport function getUser<TUser extends BackendUser = BackendUser>(\n c: Context,\n): TUser | null {\n return c.get(AUTH_USER_KEY) as TUser | null;\n}\n\n/**\n * Helper to get the auth controller from context.\n */\nexport function getAuthController<TUser extends BackendUser = BackendUser>(\n c: Context,\n): BackendAuthController<TUser> {\n return c.get(AUTH_CONTROLLER_KEY) as BackendAuthController<TUser>;\n}\n","import type {\n InferRequestBody,\n InferRequestParams,\n InferResponseBody,\n RequestSchema,\n} from \"@nubase/core\";\nimport type { Context } from \"hono\";\nimport type { ContentfulStatusCode } from \"hono/utils/http-status\";\nimport { AUTH_USER_KEY } from \"./auth/middleware\";\nimport type { AuthLevel, BackendUser } from \"./auth/types\";\n\n/**\n * Custom HTTP error class that allows handlers to throw errors with specific status codes.\n * Use this to return proper HTTP error responses instead of generic 500 errors.\n *\n * @example\n * throw new HttpError(401, \"Invalid username or password\");\n * throw new HttpError(404, \"Resource not found\");\n * throw new HttpError(403, \"Access denied\");\n */\nexport class HttpError extends Error {\n constructor(\n public statusCode: ContentfulStatusCode,\n message: string,\n ) {\n super(message);\n this.name = \"HttpError\";\n }\n}\n\n/**\n * URL Parameter Coercion System (Backend)\n *\n * **The Problem:**\n * URL path parameters always arrive as strings from HTTP requests,\n * but schemas expect typed values (numbers, booleans).\n *\n * **The Solution:**\n * We use the schema's `toZodWithCoercion()` method which leverages Zod's\n * built-in coercion to automatically convert string values to expected types.\n *\n * **Example:**\n * - URL: `/tickets/37` → params: { id: \"37\" }\n * - Schema expects: { id: number }\n * - toZodWithCoercion() converts: { id: 37 }\n */\n\n/**\n * Context provided to typed handlers.\n *\n * @template T - The request schema type\n * @template TUser - The user type (when auth is required/optional)\n */\nexport type TypedHandlerContext<\n T extends RequestSchema,\n TUser extends BackendUser | null = null,\n> = {\n params: InferRequestParams<T>;\n body: InferRequestBody<T>;\n ctx: Context;\n /**\n * The authenticated user.\n * - When auth is 'required': TUser (guaranteed to exist)\n * - When auth is 'optional': TUser | null\n * - When auth is 'none': not present (null)\n */\n user: TUser;\n};\n\n/**\n * Handler function type for typed HTTP handlers.\n */\nexport type TypedHandler<\n T extends RequestSchema,\n TUser extends BackendUser | null = null,\n> = (context: TypedHandlerContext<T, TUser>) => Promise<InferResponseBody<T>>;\n\n// Overloaded function signatures for better ergonomics\nexport function createTypedHandler<T extends RequestSchema>(\n schema: T,\n handler: TypedHandler<T>,\n): ReturnType<typeof createTypedHandlerInternal>;\n\nexport function createTypedHandler<T extends RequestSchema>(\n endpointRef: T, // Can be apiEndpoints.ticketsGetTickets\n handler: TypedHandler<T>,\n): ReturnType<typeof createTypedHandlerInternal>;\n\nexport function createTypedHandler<T extends RequestSchema>(\n schemaOrEndpoint: T,\n handler: TypedHandler<T>,\n) {\n return createTypedHandlerInternal(schemaOrEndpoint, handler);\n}\n\n// Internal implementation\nfunction createTypedHandlerInternal<\n T extends RequestSchema,\n TUser extends BackendUser | null = null,\n>(schema: T, handler: TypedHandler<T, TUser>, options?: { auth?: AuthLevel }) {\n const authLevel = options?.auth ?? \"none\";\n\n return async (c: Context) => {\n try {\n // Check authentication if required\n const user = c.get(AUTH_USER_KEY) as TUser | null;\n\n if (authLevel === \"required\" && !user) {\n return c.json({ error: \"Unauthorized\" }, 401);\n }\n\n // Parse and validate request parameters using schema's built-in coercion\n let params: InferRequestParams<T>;\n try {\n const rawParams = c.req.param();\n\n // Use toZodWithCoercion() to automatically convert string params to expected types\n params = schema.requestParams\n .toZodWithCoercion()\n .parse(rawParams) as InferRequestParams<T>;\n } catch (error) {\n return c.json(\n {\n error: \"Invalid request parameters\",\n details: error instanceof Error ? error.message : String(error),\n },\n 400,\n );\n }\n\n // Parse and validate request body\n let body: InferRequestBody<T>;\n try {\n if (schema.requestBody) {\n // Only parse body if schema defines one\n const rawBody = schema.method === \"GET\" ? {} : await c.req.json();\n body = schema.requestBody\n .toZod()\n .parse(rawBody) as InferRequestBody<T>;\n } else {\n // No request body expected\n body = undefined as InferRequestBody<T>;\n }\n } catch (error) {\n return c.json(\n {\n error: \"Invalid request body\",\n details: error instanceof Error ? error.message : String(error),\n },\n 400,\n );\n }\n\n // Call the handler with typed context\n // For 'required' auth, user is guaranteed to be non-null\n // For 'optional' auth, user may be null\n // For 'none' auth, user is null\n const result = await handler({\n params,\n body,\n ctx: c,\n user: (authLevel === \"required\" ? user : (user ?? null)) as TUser,\n });\n\n // Validate response body (optional, for development safety)\n // Use passthrough() to preserve unknown properties in the response\n // This is important for dynamic data like chart series values (e.g., { category: \"Jan\", desktop: 186, mobile: 80 })\n try {\n const responseZod = schema.responseBody.toZod();\n // Apply passthrough if it's an object schema to preserve dynamic fields\n const passthroughZod =\n \"passthrough\" in responseZod\n ? responseZod.passthrough()\n : responseZod;\n const validatedResult = passthroughZod.parse(result);\n\n // Return appropriate status code based on method\n const statusCode = schema.method === \"POST\" ? 201 : 200;\n return c.json(validatedResult, statusCode);\n } catch (error) {\n console.error(\"Response validation failed:\", error);\n // In production, you might want to return the result anyway\n // For development, this helps catch schema mismatches\n return c.json(\n {\n error: \"Internal server error\",\n details: \"Response validation failed\",\n },\n 500,\n );\n }\n } catch (error) {\n // Check if it's an HttpError with a specific status code\n if (error instanceof HttpError) {\n return c.json(\n {\n error: error.message,\n },\n error.statusCode,\n );\n }\n\n console.error(\"Handler error:\", error);\n return c.json(\n {\n error: \"Internal server error\",\n details: error instanceof Error ? error.message : String(error),\n },\n 500,\n );\n }\n };\n}\n\nexport type TypedRouteDefinition<T extends RequestSchema> = {\n schema: T;\n handler: TypedHandler<T>;\n};\n\nexport type TypedRoutes = Record<string, TypedRouteDefinition<any>>;\n\nexport function createTypedRoutes<T extends TypedRoutes>(routes: T) {\n const honoHandlers: Record<\n string,\n ReturnType<typeof createTypedHandler>\n > = {};\n\n for (const [routeName, { schema, handler }] of Object.entries(routes)) {\n honoHandlers[routeName] = createTypedHandler(schema, handler);\n }\n\n return honoHandlers;\n}\n\n/**\n * A handler created by createHttpHandler with endpoint metadata attached.\n * This allows registerHandlers to auto-register routes based on the endpoint's path and method.\n */\nexport type HttpHandler = ((c: Context) => Promise<Response>) & {\n __endpoint: RequestSchema;\n};\n\n/**\n * Options for createHttpHandler.\n */\nexport type CreateHttpHandlerOptions<\n T extends RequestSchema,\n TAuth extends AuthLevel = \"none\",\n TUser extends BackendUser = BackendUser,\n> = {\n /**\n * The endpoint schema defining request/response types.\n */\n endpoint: T;\n\n /**\n * Authentication level for this route.\n * - 'required': Request must be authenticated. Returns 401 if not. User is guaranteed in handler.\n * - 'optional': Authentication is optional. User may be null in handler.\n * - 'none': No authentication check. User is always null. (default)\n */\n auth?: TAuth;\n\n /**\n * The handler function.\n * When auth is 'required', user is guaranteed to be non-null.\n * When auth is 'optional', user may be null.\n * When auth is 'none', user is null.\n */\n handler: TypedHandler<\n T,\n TAuth extends \"required\"\n ? TUser\n : TAuth extends \"optional\"\n ? TUser | null\n : null\n >;\n};\n\n/**\n * Create a typed HTTP handler with optional authentication.\n *\n * @example\n * ```typescript\n * // No auth (default)\n * export const handleGetPublicData = createHttpHandler({\n * endpoint: apiEndpoints.getPublicData,\n * handler: async ({ body }) => {\n * return { data: 'public' };\n * },\n * });\n *\n * // Required auth - user is guaranteed\n * export const handleGetProfile = createHttpHandler({\n * endpoint: apiEndpoints.getProfile,\n * auth: 'required',\n * handler: async ({ body, user }) => {\n * // user is guaranteed to exist here\n * return { userId: user.id };\n * },\n * });\n *\n * // Optional auth - user may be null\n * export const handleGetContent = createHttpHandler({\n * endpoint: apiEndpoints.getContent,\n * auth: 'optional',\n * handler: async ({ body, user }) => {\n * if (user) {\n * return { content: 'personalized', userId: user.id };\n * }\n * return { content: 'generic' };\n * },\n * });\n * ```\n */\nexport function createHttpHandler<\n T extends RequestSchema,\n TAuth extends AuthLevel = \"none\",\n TUser extends BackendUser = BackendUser,\n>(options: CreateHttpHandlerOptions<T, TAuth, TUser>): HttpHandler {\n const { endpoint, handler, auth } = options;\n const honoHandler = createTypedHandlerInternal(\n endpoint,\n handler as TypedHandler<T, any>,\n {\n auth,\n },\n );\n\n // Attach endpoint metadata to the handler for auto-registration\n return Object.assign(honoHandler, { __endpoint: endpoint }) as HttpHandler;\n}\n\n/**\n * A record of handlers to be registered with registerHandlers.\n */\nexport type HttpHandlers = Record<string, HttpHandler>;\n\n/**\n * Register multiple HTTP handlers with a Hono app.\n * Automatically extracts path and method from each handler's endpoint metadata.\n *\n * @example\n * ```typescript\n * // In dashboard.ts\n * export const dashboardHandlers = {\n * getRevenueChart: createHttpHandler({\n * endpoint: apiEndpoints.getRevenueChart,\n * auth: \"required\",\n * handler: async () => ({ ... }),\n * }),\n * getBrowserStats: createHttpHandler({\n * endpoint: apiEndpoints.getBrowserStats,\n * auth: \"required\",\n * handler: async () => ({ ... }),\n * }),\n * };\n *\n * // In index.ts\n * registerHandlers(app, dashboardHandlers);\n * // Automatically registers:\n * // app.get(\"/dashboard/revenue-chart\", handler)\n * // app.get(\"/dashboard/browser-stats\", handler)\n * ```\n */\nexport function registerHandlers<\n TApp extends { get: any; post: any; put: any; patch: any; delete: any },\n>(app: TApp, handlers: HttpHandlers): void {\n for (const handler of Object.values(handlers)) {\n const { method, path } = handler.__endpoint;\n const methodLower = method.toLowerCase() as\n | \"get\"\n | \"post\"\n | \"put\"\n | \"patch\"\n | \"delete\";\n app[methodLower](path, handler);\n }\n}\n","/**\n * Parse a cookie header string into a key-value object.\n *\n * @param cookieHeader - The Cookie header string (e.g., \"name=value; other=123\")\n * @returns An object mapping cookie names to their values\n *\n * @example\n * ```typescript\n * const cookies = parseCookies(\"session=abc123; theme=dark\");\n * // { session: \"abc123\", theme: \"dark\" }\n * ```\n */\nexport function parseCookies(cookieHeader: string): Record<string, string> {\n const cookies: Record<string, string> = {};\n\n if (!cookieHeader) {\n return cookies;\n }\n\n for (const cookie of cookieHeader.split(\";\")) {\n const [name, ...rest] = cookie.split(\"=\");\n if (name) {\n cookies[name.trim()] = rest.join(\"=\").trim();\n }\n }\n\n return cookies;\n}\n\n/**\n * Get a specific cookie value from a cookie header string.\n *\n * @param cookieHeader - The Cookie header string\n * @param name - The cookie name to retrieve\n * @returns The cookie value or null if not found\n *\n * @example\n * ```typescript\n * const session = getCookie(\"session=abc123; theme=dark\", \"session\");\n * // \"abc123\"\n * ```\n */\nexport function getCookie(cookieHeader: string, name: string): string | null {\n const cookies = parseCookies(cookieHeader);\n return cookies[name] ?? null;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/auth/handlers.ts","../src/auth/middleware.ts","../src/typed-handlers.ts","../src/utils/cookies.ts","../src/utils/validate-environment.ts"],"sourcesContent":["import type { Context } from \"hono\";\nimport { Hono } from \"hono\";\nimport { AUTH_USER_KEY } from \"./middleware\";\nimport type { BackendAuthController, BackendUser } from \"./types\";\n\n/**\n * Options for creating auth handlers.\n */\nexport interface CreateAuthHandlersOptions<\n TUser extends BackendUser = BackendUser,\n> {\n /**\n * The auth controller instance.\n */\n controller: BackendAuthController<TUser>;\n}\n\n/**\n * Auth handlers returned by createAuthHandlers.\n */\nexport interface AuthHandlers {\n /**\n * Login handler - validates credentials and sets auth cookie.\n * Expects JSON body: { username: string, password: string }\n * Returns: { user: { id, email, username } }\n */\n login: (ctx: Context) => Promise<Response>;\n\n /**\n * Logout handler - clears the auth cookie.\n * Returns: { success: true }\n */\n logout: (ctx: Context) => Promise<Response>;\n\n /**\n * Get current user handler - returns the authenticated user or undefined.\n * Returns: { user?: { id, email, username } }\n */\n getMe: (ctx: Context) => Promise<Response>;\n\n /**\n * Pre-configured Hono router with all auth routes.\n * Mount this at /auth to get /auth/login, /auth/logout, /auth/me\n */\n routes: Hono;\n}\n\n/**\n * Create standard authentication handlers from a BackendAuthController.\n *\n * This utility reduces boilerplate by providing pre-built handlers for\n * login, logout, and get-current-user endpoints.\n *\n * @example\n * ```typescript\n * import { createAuthHandlers, createAuthMiddleware } from \"@nubase/backend\";\n *\n * const authController = new MyBackendAuthController();\n * const authHandlers = createAuthHandlers({ controller: authController });\n *\n * const app = new Hono();\n * app.use(\"*\", createAuthMiddleware({ controller: authController }));\n *\n * // Option 1: Register routes individually\n * app.post(\"/auth/login\", authHandlers.login);\n * app.post(\"/auth/logout\", authHandlers.logout);\n * app.get(\"/auth/me\", authHandlers.getMe);\n *\n * // Option 2: Mount the pre-configured router\n * app.route(\"/auth\", authHandlers.routes);\n * ```\n */\nexport function createAuthHandlers<TUser extends BackendUser = BackendUser>(\n options: CreateAuthHandlersOptions<TUser>,\n): AuthHandlers {\n const { controller } = options;\n\n const login = async (ctx: Context): Promise<Response> => {\n try {\n const body = await ctx.req.json<{ username: string; password: string }>();\n\n if (!body.username || !body.password) {\n return ctx.json({ error: \"Username and password are required\" }, 400);\n }\n\n const user = await controller.validateCredentials(\n body.username,\n body.password,\n );\n\n if (!user) {\n return ctx.json({ error: \"Invalid username or password\" }, 401);\n }\n\n const token = await controller.createToken(user);\n controller.setTokenInResponse(ctx, token);\n\n return ctx.json({ user }, 201);\n } catch (error) {\n console.error(\"Login error:\", error);\n return ctx.json({ error: \"Login failed\" }, 500);\n }\n };\n\n const logout = async (ctx: Context): Promise<Response> => {\n controller.clearTokenFromResponse(ctx);\n return ctx.json({ success: true }, 200);\n };\n\n const getMe = async (ctx: Context): Promise<Response> => {\n const user = ctx.get(AUTH_USER_KEY) as TUser | null;\n\n if (!user) {\n return ctx.json({ user: undefined }, 200);\n }\n\n return ctx.json({ user }, 200);\n };\n\n // Create pre-configured router\n const routes = new Hono();\n routes.post(\"/login\", login);\n routes.post(\"/logout\", logout);\n routes.get(\"/me\", getMe);\n\n return {\n login,\n logout,\n getMe,\n routes,\n };\n}\n","import type { Context } from \"hono\";\nimport { createMiddleware } from \"hono/factory\";\nimport type { AuthLevel, BackendAuthController, BackendUser } from \"./types\";\n\n/**\n * Context key for storing the authenticated user.\n * Use `c.get('user')` to retrieve the user in handlers.\n */\nexport const AUTH_USER_KEY = \"user\";\n\n/**\n * Context key for storing the auth controller instance.\n */\nexport const AUTH_CONTROLLER_KEY = \"authController\";\n\n/**\n * Variables added to Hono context by auth middleware.\n */\nexport interface AuthVariables<TUser extends BackendUser = BackendUser> {\n user: TUser | null;\n authController: BackendAuthController<TUser>;\n}\n\n/**\n * Options for the auth middleware.\n */\nexport interface AuthMiddlewareOptions<\n TUser extends BackendUser = BackendUser,\n> {\n /**\n * The auth controller instance to use for token verification.\n */\n controller: BackendAuthController<TUser>;\n\n /**\n * Default auth level for all routes.\n * Can be overridden per-route using createHttpHandler's auth option.\n * @default \"none\"\n */\n defaultAuthLevel?: AuthLevel;\n}\n\n/**\n * Create an authentication middleware for Hono.\n *\n * This middleware:\n * 1. Extracts the token from the request\n * 2. Verifies the token using the provided controller\n * 3. Sets the user in the context (or null if not authenticated)\n * 4. Makes the controller available in context for handlers\n *\n * @example\n * ```typescript\n * const authController = new QuestlogBackendAuthController();\n * const app = new Hono();\n *\n * // Apply to all routes\n * app.use('*', createAuthMiddleware({ controller: authController }));\n *\n * // Access user in handlers\n * app.get('/me', (c) => {\n * const user = c.get('user');\n * if (!user) return c.json({ error: 'Unauthorized' }, 401);\n * return c.json({ user });\n * });\n * ```\n */\nexport function createAuthMiddleware<TUser extends BackendUser = BackendUser>(\n options: AuthMiddlewareOptions<TUser>,\n) {\n const { controller } = options;\n\n return createMiddleware<{ Variables: AuthVariables<TUser> }>(\n async (c, next) => {\n // Store the controller in context for use by handlers\n c.set(AUTH_CONTROLLER_KEY, controller);\n\n // Extract token from request\n const token = controller.extractToken(c);\n\n // Debug logging\n const cookieHeader = c.req.header(\"Cookie\");\n console.info(\n `[Auth] ${c.req.method} ${c.req.path} - Cookie header: ${cookieHeader ? `${cookieHeader.substring(0, 50)}...` : \"(none)\"}`,\n );\n console.info(`[Auth] Token extracted: ${token ? \"yes\" : \"no\"}`);\n\n if (!token) {\n // No token present - user is not authenticated\n c.set(AUTH_USER_KEY, null);\n return next();\n }\n\n // Verify the token\n const result = await controller.verifyToken(token);\n\n console.info(\n `[Auth] Token verification: ${result.valid ? \"valid\" : `invalid - ${result.error}`}`,\n );\n\n if (result.valid) {\n c.set(AUTH_USER_KEY, result.user);\n } else {\n // Invalid token - treat as unauthenticated\n c.set(AUTH_USER_KEY, null);\n }\n\n return next();\n },\n );\n}\n\n/**\n * Middleware to require authentication.\n * Returns 401 if user is not authenticated.\n *\n * @example\n * ```typescript\n * // Protect a single route\n * app.get('/protected', requireAuth(), (c) => {\n * const user = c.get('user')!; // User is guaranteed to exist\n * return c.json({ message: `Hello ${user.username}` });\n * });\n *\n * // Protect a group of routes\n * const protected = app.basePath('/api/admin');\n * protected.use('*', requireAuth());\n * ```\n */\nexport function requireAuth<TUser extends BackendUser = BackendUser>() {\n return createMiddleware<{ Variables: AuthVariables<TUser> }>(\n async (c, next) => {\n const user = c.get(AUTH_USER_KEY);\n\n if (!user) {\n return c.json({ error: \"Unauthorized\" }, 401);\n }\n\n return next();\n },\n );\n}\n\n/**\n * Helper to get the authenticated user from context.\n * Returns null if not authenticated.\n */\nexport function getUser<TUser extends BackendUser = BackendUser>(\n c: Context,\n): TUser | null {\n return c.get(AUTH_USER_KEY) as TUser | null;\n}\n\n/**\n * Helper to get the auth controller from context.\n */\nexport function getAuthController<TUser extends BackendUser = BackendUser>(\n c: Context,\n): BackendAuthController<TUser> {\n return c.get(AUTH_CONTROLLER_KEY) as BackendAuthController<TUser>;\n}\n","import type {\n InferRequestBody,\n InferRequestParams,\n InferResponseBody,\n RequestSchema,\n} from \"@nubase/core\";\nimport type { Context } from \"hono\";\nimport type { ContentfulStatusCode } from \"hono/utils/http-status\";\nimport { AUTH_USER_KEY } from \"./auth/middleware\";\nimport type { AuthLevel, BackendUser } from \"./auth/types\";\n\n/**\n * Custom HTTP error class that allows handlers to throw errors with specific status codes.\n * Use this to return proper HTTP error responses instead of generic 500 errors.\n *\n * @example\n * throw new HttpError(401, \"Invalid username or password\");\n * throw new HttpError(404, \"Resource not found\");\n * throw new HttpError(403, \"Access denied\");\n */\nexport class HttpError extends Error {\n constructor(\n public statusCode: ContentfulStatusCode,\n message: string,\n ) {\n super(message);\n this.name = \"HttpError\";\n }\n}\n\n/**\n * URL Parameter Coercion System (Backend)\n *\n * **The Problem:**\n * URL path parameters always arrive as strings from HTTP requests,\n * but schemas expect typed values (numbers, booleans).\n *\n * **The Solution:**\n * We use the schema's `toZodWithCoercion()` method which leverages Zod's\n * built-in coercion to automatically convert string values to expected types.\n *\n * **Example:**\n * - URL: `/tickets/37` → params: { id: \"37\" }\n * - Schema expects: { id: number }\n * - toZodWithCoercion() converts: { id: 37 }\n */\n\n/**\n * Context provided to typed handlers.\n *\n * @template T - The request schema type\n * @template TUser - The user type (when auth is required/optional)\n */\nexport type TypedHandlerContext<\n T extends RequestSchema,\n TUser extends BackendUser | null = null,\n> = {\n params: InferRequestParams<T>;\n body: InferRequestBody<T>;\n ctx: Context;\n /**\n * The authenticated user.\n * - When auth is 'required': TUser (guaranteed to exist)\n * - When auth is 'optional': TUser | null\n * - When auth is 'none': not present (null)\n */\n user: TUser;\n};\n\n/**\n * Handler function type for typed HTTP handlers.\n */\nexport type TypedHandler<\n T extends RequestSchema,\n TUser extends BackendUser | null = null,\n> = (context: TypedHandlerContext<T, TUser>) => Promise<InferResponseBody<T>>;\n\n// Overloaded function signatures for better ergonomics\nexport function createTypedHandler<T extends RequestSchema>(\n schema: T,\n handler: TypedHandler<T>,\n): ReturnType<typeof createTypedHandlerInternal>;\n\nexport function createTypedHandler<T extends RequestSchema>(\n endpointRef: T, // Can be apiEndpoints.ticketsGetTickets\n handler: TypedHandler<T>,\n): ReturnType<typeof createTypedHandlerInternal>;\n\nexport function createTypedHandler<T extends RequestSchema>(\n schemaOrEndpoint: T,\n handler: TypedHandler<T>,\n) {\n return createTypedHandlerInternal(schemaOrEndpoint, handler);\n}\n\n// Internal implementation\nfunction createTypedHandlerInternal<\n T extends RequestSchema,\n TUser extends BackendUser | null = null,\n>(schema: T, handler: TypedHandler<T, TUser>, options?: { auth?: AuthLevel }) {\n const authLevel = options?.auth ?? \"none\";\n\n return async (c: Context) => {\n try {\n // Check authentication if required\n const user = c.get(AUTH_USER_KEY) as TUser | null;\n\n if (authLevel === \"required\" && !user) {\n return c.json({ error: \"Unauthorized\" }, 401);\n }\n\n // Parse and validate request parameters using schema's built-in coercion\n let params: InferRequestParams<T>;\n try {\n // Merge path params (e.g., :id from /tickets/:id) and query params (e.g., ?q=admin)\n const pathParams = c.req.param();\n const queryParams = c.req.query();\n const rawParams = { ...pathParams, ...queryParams };\n\n // Use toZodWithCoercion() to automatically convert string params to expected types\n params = schema.requestParams\n .toZodWithCoercion()\n .parse(rawParams) as InferRequestParams<T>;\n } catch (error) {\n return c.json(\n {\n error: \"Invalid request parameters\",\n details: error instanceof Error ? error.message : String(error),\n },\n 400,\n );\n }\n\n // Parse and validate request body\n let body: InferRequestBody<T>;\n try {\n if (schema.requestBody) {\n // Only parse body if schema defines one\n const rawBody = schema.method === \"GET\" ? {} : await c.req.json();\n body = schema.requestBody\n .toZod()\n .parse(rawBody) as InferRequestBody<T>;\n } else {\n // No request body expected\n body = undefined as InferRequestBody<T>;\n }\n } catch (error) {\n return c.json(\n {\n error: \"Invalid request body\",\n details: error instanceof Error ? error.message : String(error),\n },\n 400,\n );\n }\n\n // Call the handler with typed context\n // For 'required' auth, user is guaranteed to be non-null\n // For 'optional' auth, user may be null\n // For 'none' auth, user is null\n const result = await handler({\n params,\n body,\n ctx: c,\n user: (authLevel === \"required\" ? user : (user ?? null)) as TUser,\n });\n\n // Validate response body (optional, for development safety)\n // Use passthrough() to preserve unknown properties in the response\n // This is important for dynamic data like chart series values (e.g., { category: \"Jan\", desktop: 186, mobile: 80 })\n try {\n const responseZod = schema.responseBody.toZod();\n // Apply passthrough if it's an object schema to preserve dynamic fields\n const passthroughZod =\n \"passthrough\" in responseZod\n ? responseZod.passthrough()\n : responseZod;\n const validatedResult = passthroughZod.parse(result);\n\n // Return appropriate status code based on method\n const statusCode = schema.method === \"POST\" ? 201 : 200;\n return c.json(validatedResult, statusCode);\n } catch (error) {\n console.error(\"Response validation failed:\", error);\n // In production, you might want to return the result anyway\n // For development, this helps catch schema mismatches\n return c.json(\n {\n error: \"Internal server error\",\n details: \"Response validation failed\",\n },\n 500,\n );\n }\n } catch (error) {\n // Check if it's an HttpError with a specific status code\n if (error instanceof HttpError) {\n return c.json(\n {\n error: error.message,\n },\n error.statusCode,\n );\n }\n\n console.error(\"Handler error:\", error);\n return c.json(\n {\n error: \"Internal server error\",\n details: error instanceof Error ? error.message : String(error),\n },\n 500,\n );\n }\n };\n}\n\nexport type TypedRouteDefinition<T extends RequestSchema> = {\n schema: T;\n handler: TypedHandler<T>;\n};\n\nexport type TypedRoutes = Record<string, TypedRouteDefinition<any>>;\n\nexport function createTypedRoutes<T extends TypedRoutes>(routes: T) {\n const honoHandlers: Record<\n string,\n ReturnType<typeof createTypedHandler>\n > = {};\n\n for (const [routeName, { schema, handler }] of Object.entries(routes)) {\n honoHandlers[routeName] = createTypedHandler(schema, handler);\n }\n\n return honoHandlers;\n}\n\n/**\n * A handler created by createHttpHandler with endpoint metadata attached.\n * This allows registerHandlers to auto-register routes based on the endpoint's path and method.\n */\nexport type HttpHandler = ((c: Context) => Promise<Response>) & {\n __endpoint: RequestSchema;\n};\n\n/**\n * Options for createHttpHandler.\n */\nexport type CreateHttpHandlerOptions<\n T extends RequestSchema,\n TAuth extends AuthLevel = \"none\",\n TUser extends BackendUser = BackendUser,\n> = {\n /**\n * The endpoint schema defining request/response types.\n */\n endpoint: T;\n\n /**\n * Authentication level for this route.\n * - 'required': Request must be authenticated. Returns 401 if not. User is guaranteed in handler.\n * - 'optional': Authentication is optional. User may be null in handler.\n * - 'none': No authentication check. User is always null. (default)\n */\n auth?: TAuth;\n\n /**\n * The handler function.\n * When auth is 'required', user is guaranteed to be non-null.\n * When auth is 'optional', user may be null.\n * When auth is 'none', user is null.\n */\n handler: TypedHandler<\n T,\n TAuth extends \"required\"\n ? TUser\n : TAuth extends \"optional\"\n ? TUser | null\n : null\n >;\n};\n\n/**\n * Create a typed HTTP handler with optional authentication.\n *\n * @example\n * ```typescript\n * // No auth (default)\n * export const handleGetPublicData = createHttpHandler({\n * endpoint: apiEndpoints.getPublicData,\n * handler: async ({ body }) => {\n * return { data: 'public' };\n * },\n * });\n *\n * // Required auth - user is guaranteed\n * export const handleGetProfile = createHttpHandler({\n * endpoint: apiEndpoints.getProfile,\n * auth: 'required',\n * handler: async ({ body, user }) => {\n * // user is guaranteed to exist here\n * return { userId: user.id };\n * },\n * });\n *\n * // Optional auth - user may be null\n * export const handleGetContent = createHttpHandler({\n * endpoint: apiEndpoints.getContent,\n * auth: 'optional',\n * handler: async ({ body, user }) => {\n * if (user) {\n * return { content: 'personalized', userId: user.id };\n * }\n * return { content: 'generic' };\n * },\n * });\n * ```\n */\nexport function createHttpHandler<\n T extends RequestSchema,\n TAuth extends AuthLevel = \"none\",\n TUser extends BackendUser = BackendUser,\n>(options: CreateHttpHandlerOptions<T, TAuth, TUser>): HttpHandler {\n const { endpoint, handler, auth } = options;\n const honoHandler = createTypedHandlerInternal(\n endpoint,\n handler as TypedHandler<T, any>,\n {\n auth,\n },\n );\n\n // Attach endpoint metadata to the handler for auto-registration\n return Object.assign(honoHandler, { __endpoint: endpoint }) as HttpHandler;\n}\n\n/**\n * A record of handlers to be registered with registerHandlers.\n */\nexport type HttpHandlers = Record<string, HttpHandler>;\n\n/**\n * Register multiple HTTP handlers with a Hono app.\n * Automatically extracts path and method from each handler's endpoint metadata.\n *\n * @example\n * ```typescript\n * // In dashboard.ts\n * export const dashboardHandlers = {\n * getRevenueChart: createHttpHandler({\n * endpoint: apiEndpoints.getRevenueChart,\n * auth: \"required\",\n * handler: async () => ({ ... }),\n * }),\n * getBrowserStats: createHttpHandler({\n * endpoint: apiEndpoints.getBrowserStats,\n * auth: \"required\",\n * handler: async () => ({ ... }),\n * }),\n * };\n *\n * // In index.ts\n * registerHandlers(app, dashboardHandlers);\n * // Automatically registers:\n * // app.get(\"/dashboard/revenue-chart\", handler)\n * // app.get(\"/dashboard/browser-stats\", handler)\n * ```\n */\nexport function registerHandlers<\n TApp extends { get: any; post: any; put: any; patch: any; delete: any },\n>(app: TApp, handlers: HttpHandlers): void {\n for (const handler of Object.values(handlers)) {\n const { method, path } = handler.__endpoint;\n const methodLower = method.toLowerCase() as\n | \"get\"\n | \"post\"\n | \"put\"\n | \"patch\"\n | \"delete\";\n app[methodLower](path, handler);\n }\n}\n","/**\n * Parse a cookie header string into a key-value object.\n *\n * @param cookieHeader - The Cookie header string (e.g., \"name=value; other=123\")\n * @returns An object mapping cookie names to their values\n *\n * @example\n * ```typescript\n * const cookies = parseCookies(\"session=abc123; theme=dark\");\n * // { session: \"abc123\", theme: \"dark\" }\n * ```\n */\nexport function parseCookies(cookieHeader: string): Record<string, string> {\n const cookies: Record<string, string> = {};\n\n if (!cookieHeader) {\n return cookies;\n }\n\n for (const cookie of cookieHeader.split(\";\")) {\n const [name, ...rest] = cookie.split(\"=\");\n if (name) {\n cookies[name.trim()] = rest.join(\"=\").trim();\n }\n }\n\n return cookies;\n}\n\n/**\n * Get a specific cookie value from a cookie header string.\n *\n * @param cookieHeader - The Cookie header string\n * @param name - The cookie name to retrieve\n * @returns The cookie value or null if not found\n *\n * @example\n * ```typescript\n * const session = getCookie(\"session=abc123; theme=dark\", \"session\");\n * // \"abc123\"\n * ```\n */\nexport function getCookie(cookieHeader: string, name: string): string | null {\n const cookies = parseCookies(cookieHeader);\n return cookies[name] ?? null;\n}\n","import { Client } from \"pg\";\n\nexport interface ValidateEnvironmentOptions {\n /**\n * The database URL to validate connectivity against.\n * Defaults to process.env.DATABASE_URL\n */\n databaseUrl?: string;\n}\n\n/**\n * Validates that the environment is properly configured and database is accessible.\n * Should be called at application startup before starting the server.\n *\n * @throws Error if DATABASE_URL is not defined or database is not accessible\n */\nexport async function validateEnvironment(\n options: ValidateEnvironmentOptions = {},\n): Promise<void> {\n const databaseUrl = options.databaseUrl ?? process.env.DATABASE_URL;\n\n if (!databaseUrl) {\n throw new Error(\n \"DATABASE_URL is not defined in the environment variables.\",\n );\n }\n\n const client = new Client({ connectionString: databaseUrl });\n\n try {\n await client.connect();\n await client.query(\"SELECT 1\");\n await client.end();\n } catch (err) {\n await client.end().catch(() => {});\n throw new Error(\n `Failed to connect to database: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nubase/backend",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
4
4
|
"description": "Backend utilities and typed handlers for nubase",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"backend",
|
|
@@ -40,11 +40,13 @@
|
|
|
40
40
|
"hono": "^4.11.1"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
|
+
"@types/pg": "^8.15.2",
|
|
43
44
|
"tsup": "^8.5.1",
|
|
44
45
|
"typescript": "^5.9.3",
|
|
45
46
|
"vitest": "^4.0.16"
|
|
46
47
|
},
|
|
47
48
|
"dependencies": {
|
|
48
|
-
"@nubase/core": "*"
|
|
49
|
+
"@nubase/core": "*",
|
|
50
|
+
"pg": "^8.16.0"
|
|
49
51
|
}
|
|
50
52
|
}
|