@nubase/backend 0.1.0 → 0.1.4

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
@@ -1,23 +1,382 @@
1
1
  import * as hono from 'hono';
2
- import { Context } from 'hono';
2
+ import { Context, Hono } from 'hono';
3
3
  import { RequestSchema, InferRequestParams, InferRequestBody, InferResponseBody } from '@nubase/core';
4
+ import { ContentfulStatusCode } from 'hono/utils/http-status';
4
5
 
5
- type TypedHandlerContext<T extends RequestSchema> = {
6
+ /**
7
+ * Represents an authenticated user on the backend.
8
+ * Generic type allows applications to define their own user shape.
9
+ */
10
+ interface BackendUser {
11
+ id: number | string;
12
+ }
13
+ /**
14
+ * Token payload that can be embedded in JWTs.
15
+ * Applications can extend this with additional claims.
16
+ */
17
+ interface TokenPayload {
18
+ /** User identifier */
19
+ userId: number | string;
20
+ /** Token issued at timestamp */
21
+ iat?: number;
22
+ /** Token expiration timestamp */
23
+ exp?: number;
24
+ }
25
+ /**
26
+ * Result of token verification.
27
+ */
28
+ type VerifyTokenResult<TUser extends BackendUser> = {
29
+ valid: true;
30
+ user: TUser;
31
+ } | {
32
+ valid: false;
33
+ error: string;
34
+ };
35
+ /**
36
+ * Authentication level for routes.
37
+ */
38
+ type AuthLevel = "required" | "optional" | "none";
39
+ /**
40
+ * BackendAuthController interface.
41
+ *
42
+ * This is the core abstraction for backend authentication in Nubase applications.
43
+ * Applications implement this interface to provide their own authentication logic.
44
+ *
45
+ * The controller handles:
46
+ * - Token extraction from requests (cookies, headers)
47
+ * - Token verification and user lookup
48
+ * - Token creation for login flows
49
+ *
50
+ * Future extensibility:
51
+ * - Token refresh
52
+ * - Token revocation (logout, security)
53
+ * - Two-factor authentication verification
54
+ * - Social login / SSO token validation
55
+ * - Session management
56
+ *
57
+ * @template TUser - The user type returned after authentication
58
+ * @template TTokenPayload - Additional claims to include in tokens
59
+ */
60
+ interface BackendAuthController<TUser extends BackendUser = BackendUser, TTokenPayload extends TokenPayload = TokenPayload> {
61
+ /**
62
+ * Extract the authentication token from a request.
63
+ * Typically looks in cookies or Authorization header.
64
+ *
65
+ * @param ctx - Hono context
66
+ * @returns The token string or null if not present
67
+ */
68
+ extractToken(ctx: Context): string | null;
69
+ /**
70
+ * Verify a token and return the authenticated user.
71
+ *
72
+ * @param token - The token to verify
73
+ * @returns Verification result with user or error
74
+ */
75
+ verifyToken(token: string): Promise<VerifyTokenResult<TUser>>;
76
+ /**
77
+ * Create a new authentication token for a user.
78
+ * Used during login to generate the JWT.
79
+ *
80
+ * @param user - The user to create a token for
81
+ * @param additionalPayload - Optional additional claims
82
+ * @returns The signed token string
83
+ */
84
+ createToken(user: TUser, additionalPayload?: Partial<TTokenPayload>): Promise<string>;
85
+ /**
86
+ * Set the authentication token in the response.
87
+ * Typically sets an HttpOnly cookie.
88
+ *
89
+ * @param ctx - Hono context
90
+ * @param token - The token to set
91
+ */
92
+ setTokenInResponse(ctx: Context, token: string): void;
93
+ /**
94
+ * Clear the authentication token from the response.
95
+ * Used during logout.
96
+ *
97
+ * @param ctx - Hono context
98
+ */
99
+ clearTokenFromResponse(ctx: Context): void;
100
+ /**
101
+ * Validate user credentials during login.
102
+ * Looks up the user by username and verifies the password.
103
+ *
104
+ * @param username - The username to look up
105
+ * @param password - The plain text password to verify
106
+ * @returns The user if credentials are valid, null otherwise
107
+ */
108
+ validateCredentials(username: string, password: string): Promise<TUser | null>;
109
+ /**
110
+ * Refresh an existing token.
111
+ * Returns a new token if the old one is valid but near expiration.
112
+ *
113
+ * @param token - The current token
114
+ * @returns New token or null if refresh not possible
115
+ */
116
+ refreshToken?(token: string): Promise<string | null>;
117
+ /**
118
+ * Revoke a token (e.g., for logout or security).
119
+ * Implementations may track revoked tokens in a blacklist.
120
+ *
121
+ * @param token - The token to revoke
122
+ */
123
+ revokeToken?(token: string): Promise<void>;
124
+ /**
125
+ * Verify a two-factor authentication code.
126
+ *
127
+ * @param userId - The user's ID
128
+ * @param code - The 2FA code to verify
129
+ * @returns Whether the code is valid
130
+ */
131
+ verify2FA?(userId: number | string, code: string): Promise<boolean>;
132
+ /**
133
+ * Check if a user requires 2FA.
134
+ *
135
+ * @param user - The user to check
136
+ * @returns Whether 2FA is required
137
+ */
138
+ requires2FA?(user: TUser): boolean;
139
+ /**
140
+ * Validate an external OAuth/SSO token.
141
+ *
142
+ * @param provider - The OAuth provider (google, github, etc.)
143
+ * @param token - The external token
144
+ * @returns The user if valid, null otherwise
145
+ */
146
+ validateExternalToken?(provider: string, token: string): Promise<TUser | null>;
147
+ /**
148
+ * Link an external OAuth provider to a user account.
149
+ *
150
+ * @param userId - The user's ID
151
+ * @param provider - The OAuth provider
152
+ * @param externalId - The external provider's user ID
153
+ */
154
+ linkExternalProvider?(userId: number | string, provider: string, externalId: string): Promise<void>;
155
+ /**
156
+ * Create a server-side session (for SSO session sync).
157
+ *
158
+ * @param user - The user to create a session for
159
+ * @returns The session ID
160
+ */
161
+ createSession?(user: TUser): Promise<string>;
162
+ /**
163
+ * Invalidate a session.
164
+ *
165
+ * @param sessionId - The session to invalidate
166
+ */
167
+ invalidateSession?(sessionId: string): Promise<void>;
168
+ }
169
+
170
+ /**
171
+ * Options for creating auth handlers.
172
+ */
173
+ interface CreateAuthHandlersOptions<TUser extends BackendUser = BackendUser> {
174
+ /**
175
+ * The auth controller instance.
176
+ */
177
+ controller: BackendAuthController<TUser>;
178
+ }
179
+ /**
180
+ * Auth handlers returned by createAuthHandlers.
181
+ */
182
+ interface AuthHandlers {
183
+ /**
184
+ * Login handler - validates credentials and sets auth cookie.
185
+ * Expects JSON body: { username: string, password: string }
186
+ * Returns: { user: { id, email, username } }
187
+ */
188
+ login: (ctx: Context) => Promise<Response>;
189
+ /**
190
+ * Logout handler - clears the auth cookie.
191
+ * Returns: { success: true }
192
+ */
193
+ logout: (ctx: Context) => Promise<Response>;
194
+ /**
195
+ * Get current user handler - returns the authenticated user or undefined.
196
+ * Returns: { user?: { id, email, username } }
197
+ */
198
+ getMe: (ctx: Context) => Promise<Response>;
199
+ /**
200
+ * Pre-configured Hono router with all auth routes.
201
+ * Mount this at /auth to get /auth/login, /auth/logout, /auth/me
202
+ */
203
+ routes: Hono;
204
+ }
205
+ /**
206
+ * Create standard authentication handlers from a BackendAuthController.
207
+ *
208
+ * This utility reduces boilerplate by providing pre-built handlers for
209
+ * login, logout, and get-current-user endpoints.
210
+ *
211
+ * @example
212
+ * ```typescript
213
+ * import { createAuthHandlers, createAuthMiddleware } from "@nubase/backend";
214
+ *
215
+ * const authController = new MyBackendAuthController();
216
+ * const authHandlers = createAuthHandlers({ controller: authController });
217
+ *
218
+ * const app = new Hono();
219
+ * app.use("*", createAuthMiddleware({ controller: authController }));
220
+ *
221
+ * // Option 1: Register routes individually
222
+ * app.post("/auth/login", authHandlers.login);
223
+ * app.post("/auth/logout", authHandlers.logout);
224
+ * app.get("/auth/me", authHandlers.getMe);
225
+ *
226
+ * // Option 2: Mount the pre-configured router
227
+ * app.route("/auth", authHandlers.routes);
228
+ * ```
229
+ */
230
+ declare function createAuthHandlers<TUser extends BackendUser = BackendUser>(options: CreateAuthHandlersOptions<TUser>): AuthHandlers;
231
+
232
+ /**
233
+ * Context key for storing the authenticated user.
234
+ * Use `c.get('user')` to retrieve the user in handlers.
235
+ */
236
+ declare const AUTH_USER_KEY = "user";
237
+ /**
238
+ * Context key for storing the auth controller instance.
239
+ */
240
+ declare const AUTH_CONTROLLER_KEY = "authController";
241
+ /**
242
+ * Variables added to Hono context by auth middleware.
243
+ */
244
+ interface AuthVariables<TUser extends BackendUser = BackendUser> {
245
+ user: TUser | null;
246
+ authController: BackendAuthController<TUser>;
247
+ }
248
+ /**
249
+ * Options for the auth middleware.
250
+ */
251
+ interface AuthMiddlewareOptions<TUser extends BackendUser = BackendUser> {
252
+ /**
253
+ * The auth controller instance to use for token verification.
254
+ */
255
+ controller: BackendAuthController<TUser>;
256
+ /**
257
+ * Default auth level for all routes.
258
+ * Can be overridden per-route using createHttpHandler's auth option.
259
+ * @default "none"
260
+ */
261
+ defaultAuthLevel?: AuthLevel;
262
+ }
263
+ /**
264
+ * Create an authentication middleware for Hono.
265
+ *
266
+ * This middleware:
267
+ * 1. Extracts the token from the request
268
+ * 2. Verifies the token using the provided controller
269
+ * 3. Sets the user in the context (or null if not authenticated)
270
+ * 4. Makes the controller available in context for handlers
271
+ *
272
+ * @example
273
+ * ```typescript
274
+ * const authController = new QuestlogBackendAuthController();
275
+ * const app = new Hono();
276
+ *
277
+ * // Apply to all routes
278
+ * app.use('*', createAuthMiddleware({ controller: authController }));
279
+ *
280
+ * // Access user in handlers
281
+ * app.get('/me', (c) => {
282
+ * const user = c.get('user');
283
+ * if (!user) return c.json({ error: 'Unauthorized' }, 401);
284
+ * return c.json({ user });
285
+ * });
286
+ * ```
287
+ */
288
+ declare function createAuthMiddleware<TUser extends BackendUser = BackendUser>(options: AuthMiddlewareOptions<TUser>): hono.MiddlewareHandler<{
289
+ Variables: AuthVariables<TUser>;
290
+ }, string, {}, Response>;
291
+ /**
292
+ * Middleware to require authentication.
293
+ * Returns 401 if user is not authenticated.
294
+ *
295
+ * @example
296
+ * ```typescript
297
+ * // Protect a single route
298
+ * app.get('/protected', requireAuth(), (c) => {
299
+ * const user = c.get('user')!; // User is guaranteed to exist
300
+ * return c.json({ message: `Hello ${user.username}` });
301
+ * });
302
+ *
303
+ * // Protect a group of routes
304
+ * const protected = app.basePath('/api/admin');
305
+ * protected.use('*', requireAuth());
306
+ * ```
307
+ */
308
+ declare function requireAuth<TUser extends BackendUser = BackendUser>(): hono.MiddlewareHandler<{
309
+ Variables: AuthVariables<TUser>;
310
+ }, string, {}, Response>;
311
+ /**
312
+ * Helper to get the authenticated user from context.
313
+ * Returns null if not authenticated.
314
+ */
315
+ declare function getUser<TUser extends BackendUser = BackendUser>(c: Context): TUser | null;
316
+ /**
317
+ * Helper to get the auth controller from context.
318
+ */
319
+ declare function getAuthController<TUser extends BackendUser = BackendUser>(c: Context): BackendAuthController<TUser>;
320
+
321
+ /**
322
+ * Custom HTTP error class that allows handlers to throw errors with specific status codes.
323
+ * Use this to return proper HTTP error responses instead of generic 500 errors.
324
+ *
325
+ * @example
326
+ * throw new HttpError(401, "Invalid username or password");
327
+ * throw new HttpError(404, "Resource not found");
328
+ * throw new HttpError(403, "Access denied");
329
+ */
330
+ declare class HttpError extends Error {
331
+ statusCode: ContentfulStatusCode;
332
+ constructor(statusCode: ContentfulStatusCode, message: string);
333
+ }
334
+ /**
335
+ * URL Parameter Coercion System (Backend)
336
+ *
337
+ * **The Problem:**
338
+ * URL path parameters always arrive as strings from HTTP requests,
339
+ * but schemas expect typed values (numbers, booleans).
340
+ *
341
+ * **The Solution:**
342
+ * We use the schema's `toZodWithCoercion()` method which leverages Zod's
343
+ * built-in coercion to automatically convert string values to expected types.
344
+ *
345
+ * **Example:**
346
+ * - URL: `/tickets/37` → params: { id: "37" }
347
+ * - Schema expects: { id: number }
348
+ * - toZodWithCoercion() converts: { id: 37 }
349
+ */
350
+ /**
351
+ * Context provided to typed handlers.
352
+ *
353
+ * @template T - The request schema type
354
+ * @template TUser - The user type (when auth is required/optional)
355
+ */
356
+ type TypedHandlerContext<T extends RequestSchema, TUser extends BackendUser | null = null> = {
6
357
  params: InferRequestParams<T>;
7
358
  body: InferRequestBody<T>;
8
359
  ctx: Context;
360
+ /**
361
+ * The authenticated user.
362
+ * - When auth is 'required': TUser (guaranteed to exist)
363
+ * - When auth is 'optional': TUser | null
364
+ * - When auth is 'none': not present (null)
365
+ */
366
+ user: TUser;
9
367
  };
10
- type TypedHandler<T extends RequestSchema> = (context: TypedHandlerContext<T>) => Promise<InferResponseBody<T>>;
368
+ /**
369
+ * Handler function type for typed HTTP handlers.
370
+ */
371
+ type TypedHandler<T extends RequestSchema, TUser extends BackendUser | null = null> = (context: TypedHandlerContext<T, TUser>) => Promise<InferResponseBody<T>>;
11
372
  declare function createTypedHandler<T extends RequestSchema>(schema: T, handler: TypedHandler<T>): ReturnType<typeof createTypedHandlerInternal>;
12
373
  declare function createTypedHandler<T extends RequestSchema>(endpointRef: T, // Can be apiEndpoints.ticketsGetTickets
13
374
  handler: TypedHandler<T>): ReturnType<typeof createTypedHandlerInternal>;
14
- declare function createTypedHandlerInternal<T extends RequestSchema>(schema: T, handler: TypedHandler<T>): (c: Context) => Promise<(Response & hono.TypedResponse<{
375
+ declare function createTypedHandlerInternal<T extends RequestSchema, TUser extends BackendUser | null = null>(schema: T, handler: TypedHandler<T, TUser>, options?: {
376
+ auth?: AuthLevel;
377
+ }): (c: Context) => Promise<(Response & hono.TypedResponse<any, 200 | 201, "json">) | (Response & hono.TypedResponse<{
15
378
  error: string;
16
- details: string;
17
- }, 400, "json">) | (Response & hono.TypedResponse<any, 200 | 201, "json">) | (Response & hono.TypedResponse<{
18
- error: string;
19
- details: string;
20
- }, 500, "json">)>;
379
+ }, 401 | 100 | 102 | 103 | 200 | 201 | 202 | 203 | 206 | 207 | 208 | 226 | 300 | 301 | 302 | 303 | 305 | 306 | 307 | 308 | 400 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 421 | 422 | 423 | 424 | 425 | 426 | 428 | 429 | 431 | 451 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 510 | 511 | -1, "json">)>;
21
380
  type TypedRouteDefinition<T extends RequestSchema> = {
22
381
  schema: T;
23
382
  handler: TypedHandler<T>;
@@ -25,10 +384,139 @@ type TypedRouteDefinition<T extends RequestSchema> = {
25
384
  type TypedRoutes = Record<string, TypedRouteDefinition<any>>;
26
385
  declare function createTypedRoutes<T extends TypedRoutes>(routes: T): Record<string, (c: Context) => Promise<(Response & hono.TypedResponse<any, 200 | 201, "json">) | (Response & hono.TypedResponse<{
27
386
  error: string;
28
- details: string;
29
- }, 400, "json">) | (Response & hono.TypedResponse<{
30
- error: string;
31
- details: string;
32
- }, 500, "json">)>>;
387
+ }, 401 | 100 | 102 | 103 | 200 | 201 | 202 | 203 | 206 | 207 | 208 | 226 | 300 | 301 | 302 | 303 | 305 | 306 | 307 | 308 | 400 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 421 | 422 | 423 | 424 | 425 | 426 | 428 | 429 | 431 | 451 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 510 | 511 | -1, "json">)>>;
388
+ /**
389
+ * A handler created by createHttpHandler with endpoint metadata attached.
390
+ * This allows registerHandlers to auto-register routes based on the endpoint's path and method.
391
+ */
392
+ type HttpHandler = ((c: Context) => Promise<Response>) & {
393
+ __endpoint: RequestSchema;
394
+ };
395
+ /**
396
+ * Options for createHttpHandler.
397
+ */
398
+ type CreateHttpHandlerOptions<T extends RequestSchema, TAuth extends AuthLevel = "none", TUser extends BackendUser = BackendUser> = {
399
+ /**
400
+ * The endpoint schema defining request/response types.
401
+ */
402
+ endpoint: T;
403
+ /**
404
+ * Authentication level for this route.
405
+ * - 'required': Request must be authenticated. Returns 401 if not. User is guaranteed in handler.
406
+ * - 'optional': Authentication is optional. User may be null in handler.
407
+ * - 'none': No authentication check. User is always null. (default)
408
+ */
409
+ auth?: TAuth;
410
+ /**
411
+ * The handler function.
412
+ * When auth is 'required', user is guaranteed to be non-null.
413
+ * When auth is 'optional', user may be null.
414
+ * When auth is 'none', user is null.
415
+ */
416
+ handler: TypedHandler<T, TAuth extends "required" ? TUser : TAuth extends "optional" ? TUser | null : null>;
417
+ };
418
+ /**
419
+ * Create a typed HTTP handler with optional authentication.
420
+ *
421
+ * @example
422
+ * ```typescript
423
+ * // No auth (default)
424
+ * export const handleGetPublicData = createHttpHandler({
425
+ * endpoint: apiEndpoints.getPublicData,
426
+ * handler: async ({ body }) => {
427
+ * return { data: 'public' };
428
+ * },
429
+ * });
430
+ *
431
+ * // Required auth - user is guaranteed
432
+ * export const handleGetProfile = createHttpHandler({
433
+ * endpoint: apiEndpoints.getProfile,
434
+ * auth: 'required',
435
+ * handler: async ({ body, user }) => {
436
+ * // user is guaranteed to exist here
437
+ * return { userId: user.id };
438
+ * },
439
+ * });
440
+ *
441
+ * // Optional auth - user may be null
442
+ * export const handleGetContent = createHttpHandler({
443
+ * endpoint: apiEndpoints.getContent,
444
+ * auth: 'optional',
445
+ * handler: async ({ body, user }) => {
446
+ * if (user) {
447
+ * return { content: 'personalized', userId: user.id };
448
+ * }
449
+ * return { content: 'generic' };
450
+ * },
451
+ * });
452
+ * ```
453
+ */
454
+ declare function createHttpHandler<T extends RequestSchema, TAuth extends AuthLevel = "none", TUser extends BackendUser = BackendUser>(options: CreateHttpHandlerOptions<T, TAuth, TUser>): HttpHandler;
455
+ /**
456
+ * A record of handlers to be registered with registerHandlers.
457
+ */
458
+ type HttpHandlers = Record<string, HttpHandler>;
459
+ /**
460
+ * Register multiple HTTP handlers with a Hono app.
461
+ * Automatically extracts path and method from each handler's endpoint metadata.
462
+ *
463
+ * @example
464
+ * ```typescript
465
+ * // In dashboard.ts
466
+ * export const dashboardHandlers = {
467
+ * getRevenueChart: createHttpHandler({
468
+ * endpoint: apiEndpoints.getRevenueChart,
469
+ * auth: "required",
470
+ * handler: async () => ({ ... }),
471
+ * }),
472
+ * getBrowserStats: createHttpHandler({
473
+ * endpoint: apiEndpoints.getBrowserStats,
474
+ * auth: "required",
475
+ * handler: async () => ({ ... }),
476
+ * }),
477
+ * };
478
+ *
479
+ * // In index.ts
480
+ * registerHandlers(app, dashboardHandlers);
481
+ * // Automatically registers:
482
+ * // app.get("/dashboard/revenue-chart", handler)
483
+ * // app.get("/dashboard/browser-stats", handler)
484
+ * ```
485
+ */
486
+ declare function registerHandlers<TApp extends {
487
+ get: any;
488
+ post: any;
489
+ put: any;
490
+ patch: any;
491
+ delete: any;
492
+ }>(app: TApp, handlers: HttpHandlers): void;
493
+
494
+ /**
495
+ * Parse a cookie header string into a key-value object.
496
+ *
497
+ * @param cookieHeader - The Cookie header string (e.g., "name=value; other=123")
498
+ * @returns An object mapping cookie names to their values
499
+ *
500
+ * @example
501
+ * ```typescript
502
+ * const cookies = parseCookies("session=abc123; theme=dark");
503
+ * // { session: "abc123", theme: "dark" }
504
+ * ```
505
+ */
506
+ declare function parseCookies(cookieHeader: string): Record<string, string>;
507
+ /**
508
+ * Get a specific cookie value from a cookie header string.
509
+ *
510
+ * @param cookieHeader - The Cookie header string
511
+ * @param name - The cookie name to retrieve
512
+ * @returns The cookie value or null if not found
513
+ *
514
+ * @example
515
+ * ```typescript
516
+ * const session = getCookie("session=abc123; theme=dark", "session");
517
+ * // "abc123"
518
+ * ```
519
+ */
520
+ declare function getCookie(cookieHeader: string, name: string): string | null;
33
521
 
34
- export { type TypedHandler, type TypedHandlerContext, type TypedRouteDefinition, type TypedRoutes, createTypedHandler, createTypedRoutes };
522
+ 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 VerifyTokenResult, createAuthHandlers, createAuthMiddleware, createHttpHandler, createTypedHandler, createTypedRoutes, getAuthController, getCookie, getUser, parseCookies, registerHandlers, requireAuth };