@digilogiclabs/platform-core 1.13.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 +186 -1
- package/dist/auth.d.ts +186 -1
- package/dist/auth.js +205 -0
- package/dist/auth.js.map +1 -1
- package/dist/auth.mjs +204 -0
- package/dist/auth.mjs.map +1 -1
- package/package.json +1 -1
package/dist/auth.d.mts
CHANGED
|
@@ -387,4 +387,189 @@ interface LazyRateLimitStoreOptions {
|
|
|
387
387
|
*/
|
|
388
388
|
declare function createLazyRateLimitStore(getRedis: () => RedisRateLimitClient | null | undefined, options?: LazyRateLimitStoreOptions): () => RateLimitStore | undefined;
|
|
389
389
|
|
|
390
|
-
|
|
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
|
@@ -387,4 +387,189 @@ interface LazyRateLimitStoreOptions {
|
|
|
387
387
|
*/
|
|
388
388
|
declare function createLazyRateLimitStore(getRedis: () => RedisRateLimitClient | null | undefined, options?: LazyRateLimitStoreOptions): () => RateLimitStore | undefined;
|
|
389
389
|
|
|
390
|
-
|
|
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,
|