@digilogiclabs/platform-core 1.12.0 → 1.14.0

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/auth.d.mts CHANGED
@@ -142,7 +142,7 @@ declare function errorResponse(error: unknown, options?: {
142
142
  */
143
143
  declare function zodErrorResponse(error: {
144
144
  issues: Array<{
145
- path: (string | number)[];
145
+ path: PropertyKey[];
146
146
  message: string;
147
147
  }>;
148
148
  }): Response;
@@ -387,4 +387,189 @@ interface LazyRateLimitStoreOptions {
387
387
  */
388
388
  declare function createLazyRateLimitStore(getRedis: () => RedisRateLimitClient | null | undefined, options?: LazyRateLimitStoreOptions): () => RateLimitStore | undefined;
389
389
 
390
- export { type FederatedLogoutConfig, type LazyRateLimitStoreOptions, RateLimitOptions, type RateLimitResult, RateLimitRule, RateLimitStore, type RedisRateLimitClient, type RedisRateLimitStoreOptions, addRateLimitHeaders, buildFederatedLogoutHandler, createLazyRateLimitStore, createRedisRateLimitStore, enforceRateLimit, errorResponse, extractBearerToken, getClientIp, isValidBearerToken, rateLimitResponse, safeValidate, verifyCronAuth, zodErrorResponse };
390
+ /**
391
+ * Composable Secure API Handler Factory
392
+ *
393
+ * A generic, dependency-injected version of DLL's `withAuthenticatedApi` pattern.
394
+ * Apps provide their own auth, rate limiting, audit, and logger implementations
395
+ * via `createSecureHandlerFactory()`, then use the returned wrappers.
396
+ *
397
+ * This avoids coupling to any specific auth library (Auth.js, Keycloak, etc.)
398
+ * while providing consistent security middleware across all apps.
399
+ *
400
+ * @example
401
+ * ```typescript
402
+ * // lib/api-security.ts — app-level setup (once per app)
403
+ * import { createSecureHandlerFactory } from '@digilogiclabs/platform-core/auth'
404
+ * import { auth } from '@/auth'
405
+ *
406
+ * export const { withPublicApi, withAuthenticatedApi, withAdminApi } =
407
+ * createSecureHandlerFactory({
408
+ * getSession: () => auth(),
409
+ * isAdmin: (session) => session?.user?.roles?.includes('admin') ?? false,
410
+ * rateLimiter: { enforce: enforceRateLimit, presets: MY_PRESETS },
411
+ * classifyError,
412
+ * })
413
+ *
414
+ * // app/api/items/route.ts — per-route usage
415
+ * export const POST = withAuthenticatedApi({
416
+ * rateLimit: 'mutation',
417
+ * validate: CreateItemSchema,
418
+ * }, async (request, { session, validated }) => {
419
+ * return Response.json({ created: true })
420
+ * })
421
+ * ```
422
+ */
423
+ /** Minimal session shape — apps extend this with their own session type */
424
+ interface SecureSession {
425
+ user?: {
426
+ id?: string;
427
+ email?: string | null;
428
+ name?: string | null;
429
+ roles?: string[];
430
+ [key: string]: unknown;
431
+ };
432
+ [key: string]: unknown;
433
+ }
434
+ /** Result from a rate limit check */
435
+ interface SecureRateLimitResult {
436
+ allowed: boolean;
437
+ limit: number;
438
+ remaining: number;
439
+ current?: number;
440
+ resetMs?: number;
441
+ }
442
+ /** Minimal logger interface */
443
+ interface SecureLogger {
444
+ info(message: string, meta?: Record<string, unknown>): void;
445
+ warn(message: string, meta?: Record<string, unknown>): void;
446
+ error(message: string, meta?: Record<string, unknown>): void;
447
+ }
448
+ /** Configuration for the secure handler factory */
449
+ interface SecureHandlerFactoryConfig<TSession extends SecureSession = SecureSession, TPresetKey extends string = string> {
450
+ /** Get the current authenticated session (e.g. from Auth.js) */
451
+ getSession: () => Promise<TSession | null>;
452
+ /** Check if a session has admin privileges */
453
+ isAdmin: (session: TSession | null) => boolean;
454
+ /** Check if a session has any of the required roles */
455
+ hasAnyRole?: (session: TSession | null, roles: string[]) => boolean;
456
+ /** Rate limiting integration */
457
+ rateLimiter?: {
458
+ /** Check and enforce rate limit — return response if blocked, null if allowed */
459
+ enforce: (request: Request, operation: string, preset: TPresetKey, userId?: string) => Promise<Response | null>;
460
+ /** Default preset for public APIs */
461
+ publicDefault?: TPresetKey;
462
+ /** Default preset for authenticated APIs */
463
+ authDefault?: TPresetKey;
464
+ /** Default preset for admin APIs */
465
+ adminDefault?: TPresetKey;
466
+ };
467
+ /** Optional: legacy admin token secret (Bearer token auth) */
468
+ adminSecret?: string;
469
+ /** Optional: audit logging */
470
+ auditLog?: (entry: {
471
+ actor: {
472
+ id: string;
473
+ type: string;
474
+ email?: string;
475
+ };
476
+ action: string;
477
+ resource?: {
478
+ type: string;
479
+ id: string;
480
+ };
481
+ outcome: "success" | "failure" | "blocked";
482
+ reason?: string;
483
+ metadata?: Record<string, unknown>;
484
+ }) => Promise<void>;
485
+ /** Optional: create a request-scoped logger */
486
+ createLogger?: (request: Request, operation: string) => SecureLogger;
487
+ /** Optional: error classifier (defaults to platform-core classifyError) */
488
+ classifyError?: (error: unknown, isDev: boolean) => {
489
+ status: number;
490
+ body: {
491
+ error: string;
492
+ code?: string;
493
+ };
494
+ };
495
+ /** Is the app running in development mode (default: NODE_ENV check) */
496
+ isDevelopment?: boolean;
497
+ }
498
+ /** Per-route config */
499
+ interface SecureRouteConfig<TValidated = unknown, TPresetKey extends string = string> {
500
+ /** Rate limit preset key */
501
+ rateLimit?: TPresetKey;
502
+ /** Skip rate limiting */
503
+ skipRateLimit?: boolean;
504
+ /** Zod schema for input validation (POST/PUT body, GET query params) */
505
+ validate?: {
506
+ safeParse: (data: unknown) => {
507
+ success: boolean;
508
+ data?: TValidated;
509
+ error?: {
510
+ issues: Array<{
511
+ path: PropertyKey[];
512
+ message: string;
513
+ }>;
514
+ flatten?: () => {
515
+ fieldErrors: Record<string, string[]>;
516
+ };
517
+ };
518
+ };
519
+ };
520
+ /** Audit action name */
521
+ audit?: string;
522
+ /** Audit resource type */
523
+ auditResource?: string;
524
+ /** Custom operation name for logging */
525
+ operation?: string;
526
+ }
527
+ /** Context passed to route handlers */
528
+ interface SecureRouteContext<TSession extends SecureSession = SecureSession, TValidated = unknown, TParams = Record<string, string>> {
529
+ /** Authenticated session (null for public APIs) */
530
+ session: TSession | null;
531
+ /** Whether request was authenticated via legacy token */
532
+ isLegacyToken: boolean;
533
+ /** Whether the user has admin privileges */
534
+ isAdmin: boolean;
535
+ /** Validated input data */
536
+ validated: TValidated;
537
+ /** Request-scoped logger */
538
+ logger: SecureLogger;
539
+ /** Request correlation ID */
540
+ requestId: string;
541
+ /** Resolved route params */
542
+ params: TParams;
543
+ }
544
+ /** Route handler function */
545
+ type SecureRouteHandler<TSession extends SecureSession = SecureSession, TValidated = unknown, TParams = Record<string, string>> = (request: Request, context: SecureRouteContext<TSession, TValidated, TParams>) => Promise<Response>;
546
+ /**
547
+ * Create a set of composable API security wrappers for your app.
548
+ *
549
+ * Returns `withPublicApi`, `withAuthenticatedApi`, `withAdminApi`,
550
+ * and `createSecureHandler` for custom auth requirements.
551
+ */
552
+ declare function createSecureHandlerFactory<TSession extends SecureSession = SecureSession, TPresetKey extends string = string>(factoryConfig: SecureHandlerFactoryConfig<TSession, TPresetKey>): {
553
+ createSecureHandler: <TValidated = unknown, TParams = Record<string, string>>(routeConfig: SecureRouteConfig<TValidated, TPresetKey> & {
554
+ requireAuth?: boolean;
555
+ requireAdmin?: boolean;
556
+ requireRoles?: string[];
557
+ allowLegacyToken?: boolean;
558
+ }, handler: SecureRouteHandler<TSession, TValidated, TParams>) => (request: Request, routeContext: {
559
+ params: Promise<TParams>;
560
+ }) => Promise<Response>;
561
+ withPublicApi: <TValidated = unknown, TParams = Record<string, string>>(config: SecureRouteConfig<TValidated, TPresetKey>, handler: SecureRouteHandler<TSession, TValidated, TParams>) => (request: Request, routeContext: {
562
+ params: Promise<TParams>;
563
+ }) => Promise<Response>;
564
+ withAuthenticatedApi: <TValidated = unknown, TParams = Record<string, string>>(config: SecureRouteConfig<TValidated, TPresetKey>, handler: SecureRouteHandler<TSession, TValidated, TParams>) => (request: Request, routeContext: {
565
+ params: Promise<TParams>;
566
+ }) => Promise<Response>;
567
+ withAdminApi: <TValidated = unknown, TParams = Record<string, string>>(config: SecureRouteConfig<TValidated, TPresetKey>, handler: SecureRouteHandler<TSession, TValidated, TParams>) => (request: Request, routeContext: {
568
+ params: Promise<TParams>;
569
+ }) => Promise<Response>;
570
+ withLegacyAdminApi: <TValidated = unknown, TParams = Record<string, string>>(config: SecureRouteConfig<TValidated, TPresetKey>, handler: SecureRouteHandler<TSession, TValidated, TParams>) => (request: Request, routeContext: {
571
+ params: Promise<TParams>;
572
+ }) => Promise<Response>;
573
+ };
574
+
575
+ export { type FederatedLogoutConfig, type LazyRateLimitStoreOptions, RateLimitOptions, type RateLimitResult, RateLimitRule, RateLimitStore, type RedisRateLimitClient, type RedisRateLimitStoreOptions, type SecureHandlerFactoryConfig, type SecureLogger, type SecureRateLimitResult, type SecureRouteConfig, type SecureRouteContext, type SecureRouteHandler, type SecureSession, addRateLimitHeaders, buildFederatedLogoutHandler, createLazyRateLimitStore, createRedisRateLimitStore, createSecureHandlerFactory, enforceRateLimit, errorResponse, extractBearerToken, getClientIp, isValidBearerToken, rateLimitResponse, safeValidate, verifyCronAuth, zodErrorResponse };
package/dist/auth.d.ts CHANGED
@@ -142,7 +142,7 @@ declare function errorResponse(error: unknown, options?: {
142
142
  */
143
143
  declare function zodErrorResponse(error: {
144
144
  issues: Array<{
145
- path: (string | number)[];
145
+ path: PropertyKey[];
146
146
  message: string;
147
147
  }>;
148
148
  }): Response;
@@ -387,4 +387,189 @@ interface LazyRateLimitStoreOptions {
387
387
  */
388
388
  declare function createLazyRateLimitStore(getRedis: () => RedisRateLimitClient | null | undefined, options?: LazyRateLimitStoreOptions): () => RateLimitStore | undefined;
389
389
 
390
- export { type FederatedLogoutConfig, type LazyRateLimitStoreOptions, RateLimitOptions, type RateLimitResult, RateLimitRule, RateLimitStore, type RedisRateLimitClient, type RedisRateLimitStoreOptions, addRateLimitHeaders, buildFederatedLogoutHandler, createLazyRateLimitStore, createRedisRateLimitStore, enforceRateLimit, errorResponse, extractBearerToken, getClientIp, isValidBearerToken, rateLimitResponse, safeValidate, verifyCronAuth, zodErrorResponse };
390
+ /**
391
+ * Composable Secure API Handler Factory
392
+ *
393
+ * A generic, dependency-injected version of DLL's `withAuthenticatedApi` pattern.
394
+ * Apps provide their own auth, rate limiting, audit, and logger implementations
395
+ * via `createSecureHandlerFactory()`, then use the returned wrappers.
396
+ *
397
+ * This avoids coupling to any specific auth library (Auth.js, Keycloak, etc.)
398
+ * while providing consistent security middleware across all apps.
399
+ *
400
+ * @example
401
+ * ```typescript
402
+ * // lib/api-security.ts — app-level setup (once per app)
403
+ * import { createSecureHandlerFactory } from '@digilogiclabs/platform-core/auth'
404
+ * import { auth } from '@/auth'
405
+ *
406
+ * export const { withPublicApi, withAuthenticatedApi, withAdminApi } =
407
+ * createSecureHandlerFactory({
408
+ * getSession: () => auth(),
409
+ * isAdmin: (session) => session?.user?.roles?.includes('admin') ?? false,
410
+ * rateLimiter: { enforce: enforceRateLimit, presets: MY_PRESETS },
411
+ * classifyError,
412
+ * })
413
+ *
414
+ * // app/api/items/route.ts — per-route usage
415
+ * export const POST = withAuthenticatedApi({
416
+ * rateLimit: 'mutation',
417
+ * validate: CreateItemSchema,
418
+ * }, async (request, { session, validated }) => {
419
+ * return Response.json({ created: true })
420
+ * })
421
+ * ```
422
+ */
423
+ /** Minimal session shape — apps extend this with their own session type */
424
+ interface SecureSession {
425
+ user?: {
426
+ id?: string;
427
+ email?: string | null;
428
+ name?: string | null;
429
+ roles?: string[];
430
+ [key: string]: unknown;
431
+ };
432
+ [key: string]: unknown;
433
+ }
434
+ /** Result from a rate limit check */
435
+ interface SecureRateLimitResult {
436
+ allowed: boolean;
437
+ limit: number;
438
+ remaining: number;
439
+ current?: number;
440
+ resetMs?: number;
441
+ }
442
+ /** Minimal logger interface */
443
+ interface SecureLogger {
444
+ info(message: string, meta?: Record<string, unknown>): void;
445
+ warn(message: string, meta?: Record<string, unknown>): void;
446
+ error(message: string, meta?: Record<string, unknown>): void;
447
+ }
448
+ /** Configuration for the secure handler factory */
449
+ interface SecureHandlerFactoryConfig<TSession extends SecureSession = SecureSession, TPresetKey extends string = string> {
450
+ /** Get the current authenticated session (e.g. from Auth.js) */
451
+ getSession: () => Promise<TSession | null>;
452
+ /** Check if a session has admin privileges */
453
+ isAdmin: (session: TSession | null) => boolean;
454
+ /** Check if a session has any of the required roles */
455
+ hasAnyRole?: (session: TSession | null, roles: string[]) => boolean;
456
+ /** Rate limiting integration */
457
+ rateLimiter?: {
458
+ /** Check and enforce rate limit — return response if blocked, null if allowed */
459
+ enforce: (request: Request, operation: string, preset: TPresetKey, userId?: string) => Promise<Response | null>;
460
+ /** Default preset for public APIs */
461
+ publicDefault?: TPresetKey;
462
+ /** Default preset for authenticated APIs */
463
+ authDefault?: TPresetKey;
464
+ /** Default preset for admin APIs */
465
+ adminDefault?: TPresetKey;
466
+ };
467
+ /** Optional: legacy admin token secret (Bearer token auth) */
468
+ adminSecret?: string;
469
+ /** Optional: audit logging */
470
+ auditLog?: (entry: {
471
+ actor: {
472
+ id: string;
473
+ type: string;
474
+ email?: string;
475
+ };
476
+ action: string;
477
+ resource?: {
478
+ type: string;
479
+ id: string;
480
+ };
481
+ outcome: "success" | "failure" | "blocked";
482
+ reason?: string;
483
+ metadata?: Record<string, unknown>;
484
+ }) => Promise<void>;
485
+ /** Optional: create a request-scoped logger */
486
+ createLogger?: (request: Request, operation: string) => SecureLogger;
487
+ /** Optional: error classifier (defaults to platform-core classifyError) */
488
+ classifyError?: (error: unknown, isDev: boolean) => {
489
+ status: number;
490
+ body: {
491
+ error: string;
492
+ code?: string;
493
+ };
494
+ };
495
+ /** Is the app running in development mode (default: NODE_ENV check) */
496
+ isDevelopment?: boolean;
497
+ }
498
+ /** Per-route config */
499
+ interface SecureRouteConfig<TValidated = unknown, TPresetKey extends string = string> {
500
+ /** Rate limit preset key */
501
+ rateLimit?: TPresetKey;
502
+ /** Skip rate limiting */
503
+ skipRateLimit?: boolean;
504
+ /** Zod schema for input validation (POST/PUT body, GET query params) */
505
+ validate?: {
506
+ safeParse: (data: unknown) => {
507
+ success: boolean;
508
+ data?: TValidated;
509
+ error?: {
510
+ issues: Array<{
511
+ path: PropertyKey[];
512
+ message: string;
513
+ }>;
514
+ flatten?: () => {
515
+ fieldErrors: Record<string, string[]>;
516
+ };
517
+ };
518
+ };
519
+ };
520
+ /** Audit action name */
521
+ audit?: string;
522
+ /** Audit resource type */
523
+ auditResource?: string;
524
+ /** Custom operation name for logging */
525
+ operation?: string;
526
+ }
527
+ /** Context passed to route handlers */
528
+ interface SecureRouteContext<TSession extends SecureSession = SecureSession, TValidated = unknown, TParams = Record<string, string>> {
529
+ /** Authenticated session (null for public APIs) */
530
+ session: TSession | null;
531
+ /** Whether request was authenticated via legacy token */
532
+ isLegacyToken: boolean;
533
+ /** Whether the user has admin privileges */
534
+ isAdmin: boolean;
535
+ /** Validated input data */
536
+ validated: TValidated;
537
+ /** Request-scoped logger */
538
+ logger: SecureLogger;
539
+ /** Request correlation ID */
540
+ requestId: string;
541
+ /** Resolved route params */
542
+ params: TParams;
543
+ }
544
+ /** Route handler function */
545
+ type SecureRouteHandler<TSession extends SecureSession = SecureSession, TValidated = unknown, TParams = Record<string, string>> = (request: Request, context: SecureRouteContext<TSession, TValidated, TParams>) => Promise<Response>;
546
+ /**
547
+ * Create a set of composable API security wrappers for your app.
548
+ *
549
+ * Returns `withPublicApi`, `withAuthenticatedApi`, `withAdminApi`,
550
+ * and `createSecureHandler` for custom auth requirements.
551
+ */
552
+ declare function createSecureHandlerFactory<TSession extends SecureSession = SecureSession, TPresetKey extends string = string>(factoryConfig: SecureHandlerFactoryConfig<TSession, TPresetKey>): {
553
+ createSecureHandler: <TValidated = unknown, TParams = Record<string, string>>(routeConfig: SecureRouteConfig<TValidated, TPresetKey> & {
554
+ requireAuth?: boolean;
555
+ requireAdmin?: boolean;
556
+ requireRoles?: string[];
557
+ allowLegacyToken?: boolean;
558
+ }, handler: SecureRouteHandler<TSession, TValidated, TParams>) => (request: Request, routeContext: {
559
+ params: Promise<TParams>;
560
+ }) => Promise<Response>;
561
+ withPublicApi: <TValidated = unknown, TParams = Record<string, string>>(config: SecureRouteConfig<TValidated, TPresetKey>, handler: SecureRouteHandler<TSession, TValidated, TParams>) => (request: Request, routeContext: {
562
+ params: Promise<TParams>;
563
+ }) => Promise<Response>;
564
+ withAuthenticatedApi: <TValidated = unknown, TParams = Record<string, string>>(config: SecureRouteConfig<TValidated, TPresetKey>, handler: SecureRouteHandler<TSession, TValidated, TParams>) => (request: Request, routeContext: {
565
+ params: Promise<TParams>;
566
+ }) => Promise<Response>;
567
+ withAdminApi: <TValidated = unknown, TParams = Record<string, string>>(config: SecureRouteConfig<TValidated, TPresetKey>, handler: SecureRouteHandler<TSession, TValidated, TParams>) => (request: Request, routeContext: {
568
+ params: Promise<TParams>;
569
+ }) => Promise<Response>;
570
+ withLegacyAdminApi: <TValidated = unknown, TParams = Record<string, string>>(config: SecureRouteConfig<TValidated, TPresetKey>, handler: SecureRouteHandler<TSession, TValidated, TParams>) => (request: Request, routeContext: {
571
+ params: Promise<TParams>;
572
+ }) => Promise<Response>;
573
+ };
574
+
575
+ export { type FederatedLogoutConfig, type LazyRateLimitStoreOptions, RateLimitOptions, type RateLimitResult, RateLimitRule, RateLimitStore, type RedisRateLimitClient, type RedisRateLimitStoreOptions, type SecureHandlerFactoryConfig, type SecureLogger, type SecureRateLimitResult, type SecureRouteConfig, type SecureRouteContext, type SecureRouteHandler, type SecureSession, addRateLimitHeaders, buildFederatedLogoutHandler, createLazyRateLimitStore, createRedisRateLimitStore, createSecureHandlerFactory, enforceRateLimit, errorResponse, extractBearerToken, getClientIp, isValidBearerToken, rateLimitResponse, safeValidate, verifyCronAuth, zodErrorResponse };
package/dist/auth.js CHANGED
@@ -64,6 +64,7 @@ __export(auth_exports, {
64
64
  createMemoryRateLimitStore: () => createMemoryRateLimitStore,
65
65
  createRedisRateLimitStore: () => createRedisRateLimitStore,
66
66
  createSafeTextSchema: () => createSafeTextSchema,
67
+ createSecureHandlerFactory: () => createSecureHandlerFactory,
67
68
  detectStage: () => detectStage,
68
69
  enforceRateLimit: () => enforceRateLimit,
69
70
  errorResponse: () => errorResponse,
@@ -1561,6 +1562,209 @@ function createLazyRateLimitStore(getRedis, options = {}) {
1561
1562
  };
1562
1563
  }
1563
1564
 
1565
+ // src/auth/secure-handler.ts
1566
+ function defaultLogger2(_request, operation) {
1567
+ const prefix = `[${operation}]`;
1568
+ return {
1569
+ info: (msg, meta) => console.log(prefix, msg, meta ? JSON.stringify(meta) : ""),
1570
+ warn: (msg, meta) => console.warn(prefix, msg, meta ? JSON.stringify(meta) : ""),
1571
+ error: (msg, meta) => console.error(prefix, msg, meta ? JSON.stringify(meta) : "")
1572
+ };
1573
+ }
1574
+ function createSecureHandlerFactory(factoryConfig) {
1575
+ const isDev = factoryConfig.isDevelopment ?? process.env.NODE_ENV === "development";
1576
+ const classify = factoryConfig.classifyError ?? classifyError;
1577
+ const makeLogger = factoryConfig.createLogger ?? defaultLogger2;
1578
+ function getRequestId(request) {
1579
+ const headers = request.headers;
1580
+ return headers.get("x-request-id") || headers.get("x-correlation-id") || crypto.randomUUID();
1581
+ }
1582
+ function checkLegacyToken(request) {
1583
+ if (!factoryConfig.adminSecret) return false;
1584
+ const auth = request.headers.get("authorization");
1585
+ if (!auth?.startsWith("Bearer ")) return false;
1586
+ return constantTimeEqual(auth.slice(7).trim(), factoryConfig.adminSecret);
1587
+ }
1588
+ function createSecureHandler(routeConfig, handler) {
1589
+ return async (request, routeContext) => {
1590
+ const requestId = getRequestId(request);
1591
+ const operation = routeConfig.operation || `${request.method} ${new URL(request.url).pathname}`;
1592
+ const log = makeLogger(request, operation);
1593
+ const params = await routeContext.params;
1594
+ let session = null;
1595
+ let isAdmin = false;
1596
+ let isLegacyToken = false;
1597
+ try {
1598
+ if (routeConfig.requireAuth || routeConfig.requireAdmin || routeConfig.requireRoles?.length) {
1599
+ session = await factoryConfig.getSession();
1600
+ if (session?.user) {
1601
+ isAdmin = factoryConfig.isAdmin(session);
1602
+ } else if (routeConfig.allowLegacyToken && checkLegacyToken(request)) {
1603
+ isLegacyToken = true;
1604
+ isAdmin = true;
1605
+ } else {
1606
+ log.warn("Unauthorized access attempt");
1607
+ return Response.json(
1608
+ { error: "Unauthorized" },
1609
+ { status: 401, headers: { "X-Request-ID": requestId } }
1610
+ );
1611
+ }
1612
+ }
1613
+ if (routeConfig.requireAdmin && !isLegacyToken && !isAdmin) {
1614
+ log.warn("Admin access denied");
1615
+ return Response.json(
1616
+ { error: "Forbidden: Admin access required" },
1617
+ { status: 403, headers: { "X-Request-ID": requestId } }
1618
+ );
1619
+ }
1620
+ if (routeConfig.requireRoles?.length && !isLegacyToken) {
1621
+ const hasRole2 = factoryConfig.hasAnyRole ? factoryConfig.hasAnyRole(session, routeConfig.requireRoles) : routeConfig.requireRoles.some(
1622
+ (r) => session?.user?.roles?.includes(r)
1623
+ );
1624
+ if (!hasRole2) {
1625
+ log.warn("Role-based access denied");
1626
+ return Response.json(
1627
+ { error: "Forbidden: Insufficient permissions" },
1628
+ { status: 403, headers: { "X-Request-ID": requestId } }
1629
+ );
1630
+ }
1631
+ }
1632
+ if (!routeConfig.skipRateLimit && routeConfig.rateLimit && factoryConfig.rateLimiter) {
1633
+ const userId = session?.user?.id ?? void 0;
1634
+ const blocked = await factoryConfig.rateLimiter.enforce(
1635
+ request,
1636
+ operation,
1637
+ routeConfig.rateLimit,
1638
+ userId
1639
+ );
1640
+ if (blocked) return blocked;
1641
+ }
1642
+ let validated = {};
1643
+ if (routeConfig.validate) {
1644
+ let input;
1645
+ if (request.method === "GET") {
1646
+ const url = new URL(request.url);
1647
+ const qs = {};
1648
+ url.searchParams.forEach((v, k) => qs[k] = v);
1649
+ input = qs;
1650
+ } else {
1651
+ const ct = request.headers.get("content-type");
1652
+ input = ct?.includes("application/json") ? await request.json() : {};
1653
+ }
1654
+ const result = routeConfig.validate.safeParse(input);
1655
+ if (!result.success) {
1656
+ const fieldErrors = result.error?.flatten ? result.error.flatten().fieldErrors : void 0;
1657
+ return Response.json(
1658
+ { error: "Validation error", errors: fieldErrors },
1659
+ { status: 400, headers: { "X-Request-ID": requestId } }
1660
+ );
1661
+ }
1662
+ validated = result.data;
1663
+ }
1664
+ const ctx = {
1665
+ session,
1666
+ isLegacyToken,
1667
+ isAdmin,
1668
+ validated,
1669
+ logger: log,
1670
+ requestId,
1671
+ params
1672
+ };
1673
+ const response = await handler(request, ctx);
1674
+ response.headers.set("X-Request-ID", requestId);
1675
+ if (routeConfig.audit && factoryConfig.auditLog) {
1676
+ const actorId = isLegacyToken ? "admin_token" : session?.user?.id || "anonymous";
1677
+ await factoryConfig.auditLog({
1678
+ actor: {
1679
+ id: actorId,
1680
+ type: isLegacyToken ? "admin" : "user",
1681
+ email: session?.user?.email ?? void 0
1682
+ },
1683
+ action: routeConfig.audit,
1684
+ resource: routeConfig.auditResource ? { type: routeConfig.auditResource, id: "unknown" } : void 0,
1685
+ outcome: "success"
1686
+ }).catch(() => {
1687
+ });
1688
+ }
1689
+ return response;
1690
+ } catch (error) {
1691
+ log.error("Request handler error", {
1692
+ error: error instanceof Error ? error.message : String(error)
1693
+ });
1694
+ if (routeConfig.audit && factoryConfig.auditLog) {
1695
+ await factoryConfig.auditLog({
1696
+ actor: {
1697
+ id: session?.user?.id || "unknown",
1698
+ type: "user"
1699
+ },
1700
+ action: routeConfig.audit,
1701
+ outcome: "failure",
1702
+ reason: error instanceof Error ? error.message : "Unknown error"
1703
+ }).catch(() => {
1704
+ });
1705
+ }
1706
+ const { status, body } = classify(error, isDev);
1707
+ return Response.json(
1708
+ { error: body.error, code: body.code },
1709
+ { status, headers: { "X-Request-ID": requestId } }
1710
+ );
1711
+ }
1712
+ };
1713
+ }
1714
+ function withPublicApi(config, handler) {
1715
+ return createSecureHandler(
1716
+ {
1717
+ rateLimit: factoryConfig.rateLimiter?.publicDefault,
1718
+ ...config,
1719
+ requireAuth: false,
1720
+ requireAdmin: false
1721
+ },
1722
+ handler
1723
+ );
1724
+ }
1725
+ function withAuthenticatedApi(config, handler) {
1726
+ return createSecureHandler(
1727
+ {
1728
+ rateLimit: factoryConfig.rateLimiter?.authDefault,
1729
+ ...config,
1730
+ requireAuth: true,
1731
+ requireAdmin: false
1732
+ },
1733
+ handler
1734
+ );
1735
+ }
1736
+ function withAdminApi(config, handler) {
1737
+ return createSecureHandler(
1738
+ {
1739
+ rateLimit: factoryConfig.rateLimiter?.adminDefault,
1740
+ ...config,
1741
+ requireAuth: true,
1742
+ requireAdmin: true
1743
+ },
1744
+ handler
1745
+ );
1746
+ }
1747
+ function withLegacyAdminApi(config, handler) {
1748
+ return createSecureHandler(
1749
+ {
1750
+ rateLimit: factoryConfig.rateLimiter?.adminDefault,
1751
+ ...config,
1752
+ requireAuth: true,
1753
+ requireAdmin: true,
1754
+ allowLegacyToken: true
1755
+ },
1756
+ handler
1757
+ );
1758
+ }
1759
+ return {
1760
+ createSecureHandler,
1761
+ withPublicApi,
1762
+ withAuthenticatedApi,
1763
+ withAdminApi,
1764
+ withLegacyAdminApi
1765
+ };
1766
+ }
1767
+
1564
1768
  // src/env.ts
1565
1769
  function getRequiredEnv(key) {
1566
1770
  const value = process.env[key];
@@ -1697,6 +1901,7 @@ function getEnvSummary(keys) {
1697
1901
  createMemoryRateLimitStore,
1698
1902
  createRedisRateLimitStore,
1699
1903
  createSafeTextSchema,
1904
+ createSecureHandlerFactory,
1700
1905
  detectStage,
1701
1906
  enforceRateLimit,
1702
1907
  errorResponse,