@nubase/backend 0.1.22 → 0.1.24

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 CHANGED
@@ -491,6 +491,80 @@ declare function registerHandlers<TApp extends {
491
491
  delete: any;
492
492
  }>(app: TApp, handlers: HttpHandlers): void;
493
493
 
494
+ /**
495
+ * Options for the handler created by the factory.
496
+ */
497
+ type FactoryHandlerOptions<T extends RequestSchema, TAuth extends AuthLevel, TUser extends BackendUser> = {
498
+ /**
499
+ * Authentication level for this route.
500
+ * - 'required': Request must be authenticated. Returns 401 if not. User is guaranteed in handler.
501
+ * - 'optional': Authentication is optional. User may be null in handler.
502
+ * - 'none': No authentication check. User is always null. (default)
503
+ */
504
+ auth?: TAuth;
505
+ /**
506
+ * The handler function.
507
+ * When auth is 'required', user is guaranteed to be non-null.
508
+ * When auth is 'optional', user may be null.
509
+ * When auth is 'none', user is null.
510
+ */
511
+ handler: TypedHandler<T, TAuth extends "required" ? TUser : TAuth extends "optional" ? TUser | null : null>;
512
+ };
513
+ /**
514
+ * Configuration for createHandlerFactory.
515
+ */
516
+ type HandlerFactoryConfig<TEndpoints extends Record<string, RequestSchema>> = {
517
+ /**
518
+ * The API endpoints object mapping endpoint keys to their schemas.
519
+ */
520
+ endpoints: TEndpoints;
521
+ };
522
+ /**
523
+ * Endpoint selector function type.
524
+ * Receives the endpoints object and returns a specific endpoint schema.
525
+ */
526
+ type EndpointSelector<TEndpoints extends Record<string, RequestSchema>, T extends RequestSchema> = (endpoints: TEndpoints) => T;
527
+ /**
528
+ * Creates a pre-configured handler factory with app-specific endpoint types and user type.
529
+ *
530
+ * This eliminates repetitive boilerplate when creating HTTP handlers by:
531
+ * - Pre-configuring the user type once
532
+ * - Inferring endpoint schema from the selector function
533
+ * - Providing sensible defaults for auth level
534
+ *
535
+ * @example
536
+ * ```typescript
537
+ * // In your app's handler-factory.ts
538
+ * import { createHandlerFactory } from "@nubase/backend";
539
+ * import { apiEndpoints, type ApiEndpoints } from "my-schema";
540
+ * import type { MyUser } from "../auth";
541
+ *
542
+ * export const createHandler = createHandlerFactory<ApiEndpoints, MyUser>({
543
+ * endpoints: apiEndpoints,
544
+ * });
545
+ *
546
+ * // In your route files - use selector function for autocomplete
547
+ * export const ticketHandlers = {
548
+ * getTickets: createHandler(e => e.getTickets, {
549
+ * auth: "required",
550
+ * handler: async ({ params, user, ctx }) => {
551
+ * // user is typed as MyUser (guaranteed non-null)
552
+ * // params is typed based on getTickets schema
553
+ * return { ... };
554
+ * },
555
+ * }),
556
+ *
557
+ * // Public endpoint (no auth)
558
+ * getPublicData: createHandler(e => e.getPublicData, {
559
+ * handler: async ({ params }) => {
560
+ * return { ... };
561
+ * },
562
+ * }),
563
+ * };
564
+ * ```
565
+ */
566
+ declare function createHandlerFactory<TEndpoints extends Record<string, RequestSchema>, TUser extends BackendUser = BackendUser>(config: HandlerFactoryConfig<TEndpoints>): <TSelector extends (endpoints: TEndpoints) => RequestSchema, TAuth extends AuthLevel = "none">(selector: TSelector, options: FactoryHandlerOptions<ReturnType<TSelector>, TAuth, TUser>) => HttpHandler;
567
+
494
568
  /**
495
569
  * Parse a cookie header string into a key-value object.
496
570
  *
@@ -534,4 +608,4 @@ interface ValidateEnvironmentOptions {
534
608
  */
535
609
  declare function validateEnvironment(options?: ValidateEnvironmentOptions): Promise<void>;
536
610
 
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 };
611
+ export { AUTH_CONTROLLER_KEY, AUTH_USER_KEY, type AuthHandlers, type AuthLevel, type AuthMiddlewareOptions, type AuthVariables, type BackendAuthController, type BackendUser, type CreateAuthHandlersOptions, type CreateHttpHandlerOptions, type EndpointSelector, type FactoryHandlerOptions, type HandlerFactoryConfig, HttpError, type HttpHandler, type HttpHandlers, type TokenPayload, type TypedHandler, type TypedHandlerContext, type TypedRouteDefinition, type TypedRoutes, type ValidateEnvironmentOptions, type VerifyTokenResult, createAuthHandlers, createAuthMiddleware, createHandlerFactory, createHttpHandler, createTypedHandler, createTypedRoutes, getAuthController, getCookie, getUser, parseCookies, registerHandlers, requireAuth, validateEnvironment };
package/dist/index.d.ts CHANGED
@@ -491,6 +491,80 @@ declare function registerHandlers<TApp extends {
491
491
  delete: any;
492
492
  }>(app: TApp, handlers: HttpHandlers): void;
493
493
 
494
+ /**
495
+ * Options for the handler created by the factory.
496
+ */
497
+ type FactoryHandlerOptions<T extends RequestSchema, TAuth extends AuthLevel, TUser extends BackendUser> = {
498
+ /**
499
+ * Authentication level for this route.
500
+ * - 'required': Request must be authenticated. Returns 401 if not. User is guaranteed in handler.
501
+ * - 'optional': Authentication is optional. User may be null in handler.
502
+ * - 'none': No authentication check. User is always null. (default)
503
+ */
504
+ auth?: TAuth;
505
+ /**
506
+ * The handler function.
507
+ * When auth is 'required', user is guaranteed to be non-null.
508
+ * When auth is 'optional', user may be null.
509
+ * When auth is 'none', user is null.
510
+ */
511
+ handler: TypedHandler<T, TAuth extends "required" ? TUser : TAuth extends "optional" ? TUser | null : null>;
512
+ };
513
+ /**
514
+ * Configuration for createHandlerFactory.
515
+ */
516
+ type HandlerFactoryConfig<TEndpoints extends Record<string, RequestSchema>> = {
517
+ /**
518
+ * The API endpoints object mapping endpoint keys to their schemas.
519
+ */
520
+ endpoints: TEndpoints;
521
+ };
522
+ /**
523
+ * Endpoint selector function type.
524
+ * Receives the endpoints object and returns a specific endpoint schema.
525
+ */
526
+ type EndpointSelector<TEndpoints extends Record<string, RequestSchema>, T extends RequestSchema> = (endpoints: TEndpoints) => T;
527
+ /**
528
+ * Creates a pre-configured handler factory with app-specific endpoint types and user type.
529
+ *
530
+ * This eliminates repetitive boilerplate when creating HTTP handlers by:
531
+ * - Pre-configuring the user type once
532
+ * - Inferring endpoint schema from the selector function
533
+ * - Providing sensible defaults for auth level
534
+ *
535
+ * @example
536
+ * ```typescript
537
+ * // In your app's handler-factory.ts
538
+ * import { createHandlerFactory } from "@nubase/backend";
539
+ * import { apiEndpoints, type ApiEndpoints } from "my-schema";
540
+ * import type { MyUser } from "../auth";
541
+ *
542
+ * export const createHandler = createHandlerFactory<ApiEndpoints, MyUser>({
543
+ * endpoints: apiEndpoints,
544
+ * });
545
+ *
546
+ * // In your route files - use selector function for autocomplete
547
+ * export const ticketHandlers = {
548
+ * getTickets: createHandler(e => e.getTickets, {
549
+ * auth: "required",
550
+ * handler: async ({ params, user, ctx }) => {
551
+ * // user is typed as MyUser (guaranteed non-null)
552
+ * // params is typed based on getTickets schema
553
+ * return { ... };
554
+ * },
555
+ * }),
556
+ *
557
+ * // Public endpoint (no auth)
558
+ * getPublicData: createHandler(e => e.getPublicData, {
559
+ * handler: async ({ params }) => {
560
+ * return { ... };
561
+ * },
562
+ * }),
563
+ * };
564
+ * ```
565
+ */
566
+ declare function createHandlerFactory<TEndpoints extends Record<string, RequestSchema>, TUser extends BackendUser = BackendUser>(config: HandlerFactoryConfig<TEndpoints>): <TSelector extends (endpoints: TEndpoints) => RequestSchema, TAuth extends AuthLevel = "none">(selector: TSelector, options: FactoryHandlerOptions<ReturnType<TSelector>, TAuth, TUser>) => HttpHandler;
567
+
494
568
  /**
495
569
  * Parse a cookie header string into a key-value object.
496
570
  *
@@ -534,4 +608,4 @@ interface ValidateEnvironmentOptions {
534
608
  */
535
609
  declare function validateEnvironment(options?: ValidateEnvironmentOptions): Promise<void>;
536
610
 
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 };
611
+ export { AUTH_CONTROLLER_KEY, AUTH_USER_KEY, type AuthHandlers, type AuthLevel, type AuthMiddlewareOptions, type AuthVariables, type BackendAuthController, type BackendUser, type CreateAuthHandlersOptions, type CreateHttpHandlerOptions, type EndpointSelector, type FactoryHandlerOptions, type HandlerFactoryConfig, HttpError, type HttpHandler, type HttpHandlers, type TokenPayload, type TypedHandler, type TypedHandlerContext, type TypedRouteDefinition, type TypedRoutes, type ValidateEnvironmentOptions, type VerifyTokenResult, createAuthHandlers, createAuthMiddleware, createHandlerFactory, createHttpHandler, createTypedHandler, createTypedRoutes, getAuthController, getCookie, getUser, parseCookies, registerHandlers, requireAuth, validateEnvironment };
package/dist/index.js CHANGED
@@ -25,6 +25,7 @@ __export(index_exports, {
25
25
  HttpError: () => HttpError,
26
26
  createAuthHandlers: () => createAuthHandlers,
27
27
  createAuthMiddleware: () => createAuthMiddleware,
28
+ createHandlerFactory: () => createHandlerFactory,
28
29
  createHttpHandler: () => createHttpHandler,
29
30
  createTypedHandler: () => createTypedHandler,
30
31
  createTypedRoutes: () => createTypedRoutes,
@@ -266,6 +267,18 @@ function registerHandlers(app, handlers) {
266
267
  }
267
268
  }
268
269
 
270
+ // src/handler-factory.ts
271
+ function createHandlerFactory(config) {
272
+ return function createHandler(selector, options) {
273
+ const endpoint = selector(config.endpoints);
274
+ return createHttpHandler({
275
+ endpoint,
276
+ auth: options.auth,
277
+ handler: options.handler
278
+ });
279
+ };
280
+ }
281
+
269
282
  // src/utils/cookies.ts
270
283
  function parseCookies(cookieHeader) {
271
284
  const cookies = {};
@@ -314,6 +327,7 @@ async function validateEnvironment(options = {}) {
314
327
  HttpError,
315
328
  createAuthHandlers,
316
329
  createAuthMiddleware,
330
+ createHandlerFactory,
317
331
  createHttpHandler,
318
332
  createTypedHandler,
319
333
  createTypedRoutes,
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","../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\n // Use queries() to get all query params as arrays, then normalize\n // This handles both single values (assigneeId=1) and arrays (assigneeId=1&assigneeId=2)\n // Also handles axios-style array params (assigneeId[]=1&assigneeId[]=2)\n const queriesMap = c.req.queries();\n const queryParams: Record<string, string | string[]> = {};\n for (const [rawKey, values] of Object.entries(queriesMap)) {\n if (!values || values.length === 0) continue;\n // Strip [] suffix from keys (axios sends array params as \"param[]\")\n const key = rawKey.endsWith(\"[]\") ? rawKey.slice(0, -2) : rawKey;\n // If only one value, keep it as a string; otherwise keep as array\n const firstValue = values[0];\n if (firstValue !== undefined) {\n queryParams[key] = values.length === 1 ? firstValue : values;\n }\n }\n\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":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/auth/handlers.ts","../src/auth/middleware.ts","../src/typed-handlers.ts","../src/handler-factory.ts","../src/utils/cookies.ts","../src/utils/validate-environment.ts"],"sourcesContent":["export * from \"./auth\";\nexport * from \"./handler-factory\";\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\n // Use queries() to get all query params as arrays, then normalize\n // This handles both single values (assigneeId=1) and arrays (assigneeId=1&assigneeId=2)\n // Also handles axios-style array params (assigneeId[]=1&assigneeId[]=2)\n const queriesMap = c.req.queries();\n const queryParams: Record<string, string | string[]> = {};\n for (const [rawKey, values] of Object.entries(queriesMap)) {\n if (!values || values.length === 0) continue;\n // Strip [] suffix from keys (axios sends array params as \"param[]\")\n const key = rawKey.endsWith(\"[]\") ? rawKey.slice(0, -2) : rawKey;\n // If only one value, keep it as a string; otherwise keep as array\n const firstValue = values[0];\n if (firstValue !== undefined) {\n queryParams[key] = values.length === 1 ? firstValue : values;\n }\n }\n\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","import type { RequestSchema } from \"@nubase/core\";\nimport type { AuthLevel, BackendUser } from \"./auth/types\";\nimport {\n createHttpHandler,\n type HttpHandler,\n type TypedHandler,\n} from \"./typed-handlers\";\n\n/**\n * Options for the handler created by the factory.\n */\nexport type FactoryHandlerOptions<\n T extends RequestSchema,\n TAuth extends AuthLevel,\n TUser extends BackendUser,\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 * Configuration for createHandlerFactory.\n */\nexport type HandlerFactoryConfig<\n TEndpoints extends Record<string, RequestSchema>,\n> = {\n /**\n * The API endpoints object mapping endpoint keys to their schemas.\n */\n endpoints: TEndpoints;\n};\n\n/**\n * Endpoint selector function type.\n * Receives the endpoints object and returns a specific endpoint schema.\n */\nexport type EndpointSelector<\n TEndpoints extends Record<string, RequestSchema>,\n T extends RequestSchema,\n> = (endpoints: TEndpoints) => T;\n\n/**\n * Creates a pre-configured handler factory with app-specific endpoint types and user type.\n *\n * This eliminates repetitive boilerplate when creating HTTP handlers by:\n * - Pre-configuring the user type once\n * - Inferring endpoint schema from the selector function\n * - Providing sensible defaults for auth level\n *\n * @example\n * ```typescript\n * // In your app's handler-factory.ts\n * import { createHandlerFactory } from \"@nubase/backend\";\n * import { apiEndpoints, type ApiEndpoints } from \"my-schema\";\n * import type { MyUser } from \"../auth\";\n *\n * export const createHandler = createHandlerFactory<ApiEndpoints, MyUser>({\n * endpoints: apiEndpoints,\n * });\n *\n * // In your route files - use selector function for autocomplete\n * export const ticketHandlers = {\n * getTickets: createHandler(e => e.getTickets, {\n * auth: \"required\",\n * handler: async ({ params, user, ctx }) => {\n * // user is typed as MyUser (guaranteed non-null)\n * // params is typed based on getTickets schema\n * return { ... };\n * },\n * }),\n *\n * // Public endpoint (no auth)\n * getPublicData: createHandler(e => e.getPublicData, {\n * handler: async ({ params }) => {\n * return { ... };\n * },\n * }),\n * };\n * ```\n */\nexport function createHandlerFactory<\n TEndpoints extends Record<string, RequestSchema>,\n TUser extends BackendUser = BackendUser,\n>(config: HandlerFactoryConfig<TEndpoints>) {\n /**\n * Creates a typed HTTP handler for the specified endpoint.\n *\n * @param selector - A function that selects the endpoint from the endpoints object (e.g., `e => e.getTickets`)\n * @param options - Handler options including auth level and handler function\n * @returns An HttpHandler with endpoint metadata attached for auto-registration\n */\n return function createHandler<\n TSelector extends (endpoints: TEndpoints) => RequestSchema,\n TAuth extends AuthLevel = \"none\",\n >(\n selector: TSelector,\n options: FactoryHandlerOptions<ReturnType<TSelector>, TAuth, TUser>,\n ): HttpHandler {\n const endpoint = selector(config.endpoints);\n\n return createHttpHandler({\n endpoint,\n auth: options.auth,\n handler: options.handler as any,\n });\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
@@ -226,6 +226,18 @@ function registerHandlers(app, handlers) {
226
226
  }
227
227
  }
228
228
 
229
+ // src/handler-factory.ts
230
+ function createHandlerFactory(config) {
231
+ return function createHandler(selector, options) {
232
+ const endpoint = selector(config.endpoints);
233
+ return createHttpHandler({
234
+ endpoint,
235
+ auth: options.auth,
236
+ handler: options.handler
237
+ });
238
+ };
239
+ }
240
+
229
241
  // src/utils/cookies.ts
230
242
  function parseCookies(cookieHeader) {
231
243
  const cookies = {};
@@ -273,6 +285,7 @@ export {
273
285
  HttpError,
274
286
  createAuthHandlers,
275
287
  createAuthMiddleware,
288
+ createHandlerFactory,
276
289
  createHttpHandler,
277
290
  createTypedHandler,
278
291
  createTypedRoutes,
@@ -1 +1 @@
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\n // Use queries() to get all query params as arrays, then normalize\n // This handles both single values (assigneeId=1) and arrays (assigneeId=1&assigneeId=2)\n // Also handles axios-style array params (assigneeId[]=1&assigneeId[]=2)\n const queriesMap = c.req.queries();\n const queryParams: Record<string, string | string[]> = {};\n for (const [rawKey, values] of Object.entries(queriesMap)) {\n if (!values || values.length === 0) continue;\n // Strip [] suffix from keys (axios sends array params as \"param[]\")\n const key = rawKey.endsWith(\"[]\") ? rawKey.slice(0, -2) : rawKey;\n // If only one value, keep it as a string; otherwise keep as array\n const firstValue = values[0];\n if (firstValue !== undefined) {\n queryParams[key] = values.length === 1 ? firstValue : values;\n }\n }\n\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":[]}
1
+ {"version":3,"sources":["../src/auth/handlers.ts","../src/auth/middleware.ts","../src/typed-handlers.ts","../src/handler-factory.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\n // Use queries() to get all query params as arrays, then normalize\n // This handles both single values (assigneeId=1) and arrays (assigneeId=1&assigneeId=2)\n // Also handles axios-style array params (assigneeId[]=1&assigneeId[]=2)\n const queriesMap = c.req.queries();\n const queryParams: Record<string, string | string[]> = {};\n for (const [rawKey, values] of Object.entries(queriesMap)) {\n if (!values || values.length === 0) continue;\n // Strip [] suffix from keys (axios sends array params as \"param[]\")\n const key = rawKey.endsWith(\"[]\") ? rawKey.slice(0, -2) : rawKey;\n // If only one value, keep it as a string; otherwise keep as array\n const firstValue = values[0];\n if (firstValue !== undefined) {\n queryParams[key] = values.length === 1 ? firstValue : values;\n }\n }\n\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","import type { RequestSchema } from \"@nubase/core\";\nimport type { AuthLevel, BackendUser } from \"./auth/types\";\nimport {\n createHttpHandler,\n type HttpHandler,\n type TypedHandler,\n} from \"./typed-handlers\";\n\n/**\n * Options for the handler created by the factory.\n */\nexport type FactoryHandlerOptions<\n T extends RequestSchema,\n TAuth extends AuthLevel,\n TUser extends BackendUser,\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 * Configuration for createHandlerFactory.\n */\nexport type HandlerFactoryConfig<\n TEndpoints extends Record<string, RequestSchema>,\n> = {\n /**\n * The API endpoints object mapping endpoint keys to their schemas.\n */\n endpoints: TEndpoints;\n};\n\n/**\n * Endpoint selector function type.\n * Receives the endpoints object and returns a specific endpoint schema.\n */\nexport type EndpointSelector<\n TEndpoints extends Record<string, RequestSchema>,\n T extends RequestSchema,\n> = (endpoints: TEndpoints) => T;\n\n/**\n * Creates a pre-configured handler factory with app-specific endpoint types and user type.\n *\n * This eliminates repetitive boilerplate when creating HTTP handlers by:\n * - Pre-configuring the user type once\n * - Inferring endpoint schema from the selector function\n * - Providing sensible defaults for auth level\n *\n * @example\n * ```typescript\n * // In your app's handler-factory.ts\n * import { createHandlerFactory } from \"@nubase/backend\";\n * import { apiEndpoints, type ApiEndpoints } from \"my-schema\";\n * import type { MyUser } from \"../auth\";\n *\n * export const createHandler = createHandlerFactory<ApiEndpoints, MyUser>({\n * endpoints: apiEndpoints,\n * });\n *\n * // In your route files - use selector function for autocomplete\n * export const ticketHandlers = {\n * getTickets: createHandler(e => e.getTickets, {\n * auth: \"required\",\n * handler: async ({ params, user, ctx }) => {\n * // user is typed as MyUser (guaranteed non-null)\n * // params is typed based on getTickets schema\n * return { ... };\n * },\n * }),\n *\n * // Public endpoint (no auth)\n * getPublicData: createHandler(e => e.getPublicData, {\n * handler: async ({ params }) => {\n * return { ... };\n * },\n * }),\n * };\n * ```\n */\nexport function createHandlerFactory<\n TEndpoints extends Record<string, RequestSchema>,\n TUser extends BackendUser = BackendUser,\n>(config: HandlerFactoryConfig<TEndpoints>) {\n /**\n * Creates a typed HTTP handler for the specified endpoint.\n *\n * @param selector - A function that selects the endpoint from the endpoints object (e.g., `e => e.getTickets`)\n * @param options - Handler options including auth level and handler function\n * @returns An HttpHandler with endpoint metadata attached for auto-registration\n */\n return function createHandler<\n TSelector extends (endpoints: TEndpoints) => RequestSchema,\n TAuth extends AuthLevel = \"none\",\n >(\n selector: TSelector,\n options: FactoryHandlerOptions<ReturnType<TSelector>, TAuth, TUser>,\n ): HttpHandler {\n const endpoint = selector(config.endpoints);\n\n return createHttpHandler({\n endpoint,\n auth: options.auth,\n handler: options.handler as any,\n });\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":";AACA,SAAS,YAAY;;;ACArB,SAAS,wBAAwB;AAO1B,IAAM,gBAAgB;AAKtB,IAAM,sBAAsB;AAsD5B,SAAS,qBACd,SACA;AACA,QAAM,EAAE,WAAW,IAAI;AAEvB,SAAO;AAAA,IACL,OAAO,GAAG,SAAS;AAEjB,QAAE,IAAI,qBAAqB,UAAU;AAGrC,YAAM,QAAQ,WAAW,aAAa,CAAC;AAGvC,YAAM,eAAe,EAAE,IAAI,OAAO,QAAQ;AAC1C,cAAQ;AAAA,QACN,UAAU,EAAE,IAAI,MAAM,IAAI,EAAE,IAAI,IAAI,qBAAqB,eAAe,GAAG,aAAa,UAAU,GAAG,EAAE,CAAC,QAAQ,QAAQ;AAAA,MAC1H;AACA,cAAQ,KAAK,2BAA2B,QAAQ,QAAQ,IAAI,EAAE;AAE9D,UAAI,CAAC,OAAO;AAEV,UAAE,IAAI,eAAe,IAAI;AACzB,eAAO,KAAK;AAAA,MACd;AAGA,YAAM,SAAS,MAAM,WAAW,YAAY,KAAK;AAEjD,cAAQ;AAAA,QACN,8BAA8B,OAAO,QAAQ,UAAU,aAAa,OAAO,KAAK,EAAE;AAAA,MACpF;AAEA,UAAI,OAAO,OAAO;AAChB,UAAE,IAAI,eAAe,OAAO,IAAI;AAAA,MAClC,OAAO;AAEL,UAAE,IAAI,eAAe,IAAI;AAAA,MAC3B;AAEA,aAAO,KAAK;AAAA,IACd;AAAA,EACF;AACF;AAmBO,SAAS,cAAuD;AACrE,SAAO;AAAA,IACL,OAAO,GAAG,SAAS;AACjB,YAAM,OAAO,EAAE,IAAI,aAAa;AAEhC,UAAI,CAAC,MAAM;AACT,eAAO,EAAE,KAAK,EAAE,OAAO,eAAe,GAAG,GAAG;AAAA,MAC9C;AAEA,aAAO,KAAK;AAAA,IACd;AAAA,EACF;AACF;AAMO,SAAS,QACd,GACc;AACd,SAAO,EAAE,IAAI,aAAa;AAC5B;AAKO,SAAS,kBACd,GAC8B;AAC9B,SAAO,EAAE,IAAI,mBAAmB;AAClC;;;ADxFO,SAAS,mBACd,SACc;AACd,QAAM,EAAE,WAAW,IAAI;AAEvB,QAAM,QAAQ,OAAO,QAAoC;AACvD,QAAI;AACF,YAAM,OAAO,MAAM,IAAI,IAAI,KAA6C;AAExE,UAAI,CAAC,KAAK,YAAY,CAAC,KAAK,UAAU;AACpC,eAAO,IAAI,KAAK,EAAE,OAAO,qCAAqC,GAAG,GAAG;AAAA,MACtE;AAEA,YAAM,OAAO,MAAM,WAAW;AAAA,QAC5B,KAAK;AAAA,QACL,KAAK;AAAA,MACP;AAEA,UAAI,CAAC,MAAM;AACT,eAAO,IAAI,KAAK,EAAE,OAAO,+BAA+B,GAAG,GAAG;AAAA,MAChE;AAEA,YAAM,QAAQ,MAAM,WAAW,YAAY,IAAI;AAC/C,iBAAW,mBAAmB,KAAK,KAAK;AAExC,aAAO,IAAI,KAAK,EAAE,KAAK,GAAG,GAAG;AAAA,IAC/B,SAAS,OAAO;AACd,cAAQ,MAAM,gBAAgB,KAAK;AACnC,aAAO,IAAI,KAAK,EAAE,OAAO,eAAe,GAAG,GAAG;AAAA,IAChD;AAAA,EACF;AAEA,QAAM,SAAS,OAAO,QAAoC;AACxD,eAAW,uBAAuB,GAAG;AACrC,WAAO,IAAI,KAAK,EAAE,SAAS,KAAK,GAAG,GAAG;AAAA,EACxC;AAEA,QAAM,QAAQ,OAAO,QAAoC;AACvD,UAAM,OAAO,IAAI,IAAI,aAAa;AAElC,QAAI,CAAC,MAAM;AACT,aAAO,IAAI,KAAK,EAAE,MAAM,OAAU,GAAG,GAAG;AAAA,IAC1C;AAEA,WAAO,IAAI,KAAK,EAAE,KAAK,GAAG,GAAG;AAAA,EAC/B;AAGA,QAAM,SAAS,IAAI,KAAK;AACxB,SAAO,KAAK,UAAU,KAAK;AAC3B,SAAO,KAAK,WAAW,MAAM;AAC7B,SAAO,IAAI,OAAO,KAAK;AAEvB,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;;;AE/GO,IAAM,YAAN,cAAwB,MAAM;AAAA,EACnC,YACS,YACP,SACA;AACA,UAAM,OAAO;AAHN;AAIP,SAAK,OAAO;AAAA,EACd;AACF;AA4DO,SAAS,mBACd,kBACA,SACA;AACA,SAAO,2BAA2B,kBAAkB,OAAO;AAC7D;AAGA,SAAS,2BAGP,QAAW,SAAiC,SAAgC;AAC5E,QAAM,YAAY,SAAS,QAAQ;AAEnC,SAAO,OAAO,MAAe;AAC3B,QAAI;AAEF,YAAM,OAAO,EAAE,IAAI,aAAa;AAEhC,UAAI,cAAc,cAAc,CAAC,MAAM;AACrC,eAAO,EAAE,KAAK,EAAE,OAAO,eAAe,GAAG,GAAG;AAAA,MAC9C;AAGA,UAAI;AACJ,UAAI;AAEF,cAAM,aAAa,EAAE,IAAI,MAAM;AAK/B,cAAM,aAAa,EAAE,IAAI,QAAQ;AACjC,cAAM,cAAiD,CAAC;AACxD,mBAAW,CAAC,QAAQ,MAAM,KAAK,OAAO,QAAQ,UAAU,GAAG;AACzD,cAAI,CAAC,UAAU,OAAO,WAAW,EAAG;AAEpC,gBAAM,MAAM,OAAO,SAAS,IAAI,IAAI,OAAO,MAAM,GAAG,EAAE,IAAI;AAE1D,gBAAM,aAAa,OAAO,CAAC;AAC3B,cAAI,eAAe,QAAW;AAC5B,wBAAY,GAAG,IAAI,OAAO,WAAW,IAAI,aAAa;AAAA,UACxD;AAAA,QACF;AAEA,cAAM,YAAY,EAAE,GAAG,YAAY,GAAG,YAAY;AAGlD,iBAAS,OAAO,cACb,kBAAkB,EAClB,MAAM,SAAS;AAAA,MACpB,SAAS,OAAO;AACd,eAAO,EAAE;AAAA,UACP;AAAA,YACE,OAAO;AAAA,YACP,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAAA,UAChE;AAAA,UACA;AAAA,QACF;AAAA,MACF;AAGA,UAAI;AACJ,UAAI;AACF,YAAI,OAAO,aAAa;AAEtB,gBAAM,UAAU,OAAO,WAAW,QAAQ,CAAC,IAAI,MAAM,EAAE,IAAI,KAAK;AAChE,iBAAO,OAAO,YACX,MAAM,EACN,MAAM,OAAO;AAAA,QAClB,OAAO;AAEL,iBAAO;AAAA,QACT;AAAA,MACF,SAAS,OAAO;AACd,eAAO,EAAE;AAAA,UACP;AAAA,YACE,OAAO;AAAA,YACP,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAAA,UAChE;AAAA,UACA;AAAA,QACF;AAAA,MACF;AAMA,YAAM,SAAS,MAAM,QAAQ;AAAA,QAC3B;AAAA,QACA;AAAA,QACA,KAAK;AAAA,QACL,MAAO,cAAc,aAAa,OAAQ,QAAQ;AAAA,MACpD,CAAC;AAKD,UAAI;AACF,cAAM,cAAc,OAAO,aAAa,MAAM;AAE9C,cAAM,iBACJ,iBAAiB,cACb,YAAY,YAAY,IACxB;AACN,cAAM,kBAAkB,eAAe,MAAM,MAAM;AAGnD,cAAM,aAAa,OAAO,WAAW,SAAS,MAAM;AACpD,eAAO,EAAE,KAAK,iBAAiB,UAAU;AAAA,MAC3C,SAAS,OAAO;AACd,gBAAQ,MAAM,+BAA+B,KAAK;AAGlD,eAAO,EAAE;AAAA,UACP;AAAA,YACE,OAAO;AAAA,YACP,SAAS;AAAA,UACX;AAAA,UACA;AAAA,QACF;AAAA,MACF;AAAA,IACF,SAAS,OAAO;AAEd,UAAI,iBAAiB,WAAW;AAC9B,eAAO,EAAE;AAAA,UACP;AAAA,YACE,OAAO,MAAM;AAAA,UACf;AAAA,UACA,MAAM;AAAA,QACR;AAAA,MACF;AAEA,cAAQ,MAAM,kBAAkB,KAAK;AACrC,aAAO,EAAE;AAAA,QACP;AAAA,UACE,OAAO;AAAA,UACP,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAAA,QAChE;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;AASO,SAAS,kBAAyC,QAAW;AAClE,QAAM,eAGF,CAAC;AAEL,aAAW,CAAC,WAAW,EAAE,QAAQ,QAAQ,CAAC,KAAK,OAAO,QAAQ,MAAM,GAAG;AACrE,iBAAa,SAAS,IAAI,mBAAmB,QAAQ,OAAO;AAAA,EAC9D;AAEA,SAAO;AACT;AAmFO,SAAS,kBAId,SAAiE;AACjE,QAAM,EAAE,UAAU,SAAS,KAAK,IAAI;AACpC,QAAM,cAAc;AAAA,IAClB;AAAA,IACA;AAAA,IACA;AAAA,MACE;AAAA,IACF;AAAA,EACF;AAGA,SAAO,OAAO,OAAO,aAAa,EAAE,YAAY,SAAS,CAAC;AAC5D;AAkCO,SAAS,iBAEd,KAAW,UAA8B;AACzC,aAAW,WAAW,OAAO,OAAO,QAAQ,GAAG;AAC7C,UAAM,EAAE,QAAQ,KAAK,IAAI,QAAQ;AACjC,UAAM,cAAc,OAAO,YAAY;AAMvC,QAAI,WAAW,EAAE,MAAM,OAAO;AAAA,EAChC;AACF;;;ACzSO,SAAS,qBAGd,QAA0C;AAQ1C,SAAO,SAAS,cAId,UACA,SACa;AACb,UAAM,WAAW,SAAS,OAAO,SAAS;AAE1C,WAAO,kBAAkB;AAAA,MACvB;AAAA,MACA,MAAM,QAAQ;AAAA,MACd,SAAS,QAAQ;AAAA,IACnB,CAAC;AAAA,EACH;AACF;;;AClHO,SAAS,aAAa,cAA8C;AACzE,QAAM,UAAkC,CAAC;AAEzC,MAAI,CAAC,cAAc;AACjB,WAAO;AAAA,EACT;AAEA,aAAW,UAAU,aAAa,MAAM,GAAG,GAAG;AAC5C,UAAM,CAAC,MAAM,GAAG,IAAI,IAAI,OAAO,MAAM,GAAG;AACxC,QAAI,MAAM;AACR,cAAQ,KAAK,KAAK,CAAC,IAAI,KAAK,KAAK,GAAG,EAAE,KAAK;AAAA,IAC7C;AAAA,EACF;AAEA,SAAO;AACT;AAeO,SAAS,UAAU,cAAsB,MAA6B;AAC3E,QAAM,UAAU,aAAa,YAAY;AACzC,SAAO,QAAQ,IAAI,KAAK;AAC1B;;;AC7CA,SAAS,cAAc;AAgBvB,eAAsB,oBACpB,UAAsC,CAAC,GACxB;AACf,QAAM,cAAc,QAAQ,eAAe,QAAQ,IAAI;AAEvD,MAAI,CAAC,aAAa;AAChB,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAEA,QAAM,SAAS,IAAI,OAAO,EAAE,kBAAkB,YAAY,CAAC;AAE3D,MAAI;AACF,UAAM,OAAO,QAAQ;AACrB,UAAM,OAAO,MAAM,UAAU;AAC7B,UAAM,OAAO,IAAI;AAAA,EACnB,SAAS,KAAK;AACZ,UAAM,OAAO,IAAI,EAAE,MAAM,MAAM;AAAA,IAAC,CAAC;AACjC,UAAM,IAAI;AAAA,MACR,kCAAkC,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAAA,IACpF;AAAA,EACF;AACF;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nubase/backend",
3
- "version": "0.1.22",
3
+ "version": "0.1.24",
4
4
  "description": "Backend utilities and typed handlers for nubase",
5
5
  "keywords": [
6
6
  "backend",