@markwharton/pwa-core 3.5.0 → 4.0.1

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/server.d.ts CHANGED
@@ -23,12 +23,11 @@ export declare function initAuth(config: {
23
23
  /**
24
24
  * Initializes JWT authentication from environment variables.
25
25
  * Reads JWT_SECRET from process.env.
26
- * @param minLength - Minimum required secret length (default: 32)
27
26
  * @throws Error if JWT_SECRET is missing or too short
28
27
  * @example
29
28
  * initAuthFromEnv(); // Uses process.env.JWT_SECRET
30
29
  */
31
- export declare function initAuthFromEnv(minLength?: number): void;
30
+ export declare function initAuthFromEnv(): void;
32
31
  /**
33
32
  * Gets the configured JWT secret.
34
33
  * @returns The JWT secret string
@@ -75,50 +74,31 @@ export declare function generateToken<T extends object>(payload: T, expiresIn?:
75
74
  * const apiToken = generateLongLivedToken({ machineId: 'server-1' });
76
75
  */
77
76
  export declare function generateLongLivedToken<T extends object>(payload: T, expiresInDays?: number): string;
78
- /**
79
- * Successful auth result with typed payload.
80
- */
81
- export interface AuthSuccess<T extends BaseJwtPayload> {
82
- authorized: true;
83
- payload: T;
84
- }
85
- /**
86
- * Failed auth result with HTTP response.
87
- */
88
- export interface AuthFailure {
89
- authorized: false;
90
- response: HttpResponseInit;
91
- }
92
- /**
93
- * Discriminated union for auth results.
94
- * Use `auth.authorized` to narrow the type.
95
- */
96
- export type AuthResult<T extends BaseJwtPayload> = AuthSuccess<T> | AuthFailure;
97
77
  /** Default token expiry string for generateToken (7 days) */
98
78
  export declare const DEFAULT_TOKEN_EXPIRY = "7d";
99
79
  /** Default token expiry in seconds (7 days = 604800 seconds) */
100
80
  export declare const DEFAULT_TOKEN_EXPIRY_SECONDS: number;
101
81
  /**
102
- * Validates auth header and returns typed payload or error response.
82
+ * Validates auth header and returns typed payload or error result.
103
83
  * @typeParam T - The expected payload type (extends BaseJwtPayload)
104
84
  * @param authHeader - The Authorization header value
105
- * @returns AuthResult with payload on success, or HTTP response on failure
85
+ * @returns Result with payload on success, or error with status on failure
106
86
  * @example
107
87
  * const auth = requireAuth<UsernameTokenPayload>(request.headers.get('Authorization'));
108
- * if (!auth.authorized) return auth.response;
109
- * console.log(auth.payload.username);
88
+ * if (!auth.ok) return resultToResponse(auth);
89
+ * console.log(auth.data.username);
110
90
  */
111
- export declare function requireAuth<T extends BaseJwtPayload>(authHeader: string | null): AuthResult<T>;
91
+ export declare function requireAuth<T extends BaseJwtPayload>(authHeader: string | null): Result<T>;
112
92
  /**
113
- * Requires admin role. Use with RoleTokenPayload.
93
+ * Requires admin role. Generic like requireAuth — callers can extend RoleTokenPayload.
114
94
  * @param authHeader - The Authorization header value
115
- * @returns AuthResult with RoleTokenPayload on success, or HTTP response on failure
95
+ * @returns Result with payload on success, or error with status on failure
116
96
  * @example
117
97
  * const auth = requireAdmin(request.headers.get('Authorization'));
118
- * if (!auth.authorized) return auth.response;
98
+ * if (!auth.ok) return resultToResponse(auth);
119
99
  * // User is admin
120
100
  */
121
- export declare function requireAdmin(authHeader: string | null): AuthResult<RoleTokenPayload>;
101
+ export declare function requireAdmin<T extends RoleTokenPayload = RoleTokenPayload>(authHeader: string | null): Result<T>;
122
102
  /**
123
103
  * Extracts API key from the X-API-Key header.
124
104
  * @param request - Request object with headers.get() method
@@ -259,12 +239,14 @@ export declare function initErrorHandling(config?: {
259
239
  * Initializes error handling from environment variables.
260
240
  * Currently a no-op but provides consistent API for future error config
261
241
  * (e.g., ERROR_LOG_LEVEL, ERROR_INCLUDE_STACK).
262
- * @param callback - Optional callback invoked when handleFunctionError is called (fire-and-forget)
242
+ * @param config - Optional config with onError callback invoked when handleFunctionError is called (fire-and-forget)
263
243
  * @example
264
244
  * initErrorHandlingFromEnv(); // Uses process.env automatically
265
- * initErrorHandlingFromEnv((op, msg) => sendAlert(op, msg));
245
+ * initErrorHandlingFromEnv({ onError: (op, msg) => sendAlert(op, msg) });
266
246
  */
267
- export declare function initErrorHandlingFromEnv(callback?: ErrorCallback): void;
247
+ export declare function initErrorHandlingFromEnv(config?: {
248
+ onError?: ErrorCallback;
249
+ }): void;
268
250
  /**
269
251
  * Handles unexpected errors safely by logging details and returning a generic message.
270
252
  * Use in catch blocks to avoid exposing internal error details to clients.
@@ -397,15 +379,16 @@ export declare function upsertEntity<T extends {
397
379
  }>(client: TableClient, entity: T): Promise<void>;
398
380
  /**
399
381
  * Deletes an entity from Azure Table Storage.
400
- * Returns true on success, false on error (swallows errors).
382
+ * Returns ok(true) if deleted, ok(false) if not found (not an error), err() on real failures.
401
383
  * @param client - The TableClient instance
402
384
  * @param partitionKey - The partition key
403
385
  * @param rowKey - The row key
404
- * @returns True if deleted successfully, false on error
386
+ * @returns Result with true if deleted, false if not found, or error on failure
405
387
  * @example
406
- * const deleted = await deleteEntity(client, 'session', sessionId);
388
+ * const result = await deleteEntity(client, 'session', sessionId);
389
+ * if (!result.ok) console.error(result.error);
407
390
  */
408
- export declare function deleteEntity(client: TableClient, partitionKey: string, rowKey: string): Promise<boolean>;
391
+ export declare function deleteEntity(client: TableClient, partitionKey: string, rowKey: string): Promise<Result<boolean>>;
409
392
  /**
410
393
  * Lists all entities in a partition.
411
394
  * @typeParam T - The entity type
@@ -458,8 +441,12 @@ export interface SessionAuthConfig {
458
441
  allowedDomain?: string;
459
442
  /** Emails that get isAdmin=true */
460
443
  adminEmails?: string[];
461
- /** Custom email validation callback (overrides default allowedEmails/allowedDomain check). Supports async for database lookups. */
444
+ /** Additional email validation. Called after built-in domain/admin checks reject.
445
+ * Can only widen access, never narrow it. Supports async for database lookups. */
462
446
  isEmailAllowed?: (email: string) => boolean | Promise<boolean>;
447
+ /** Transform base SessionUser into an app-specific user type.
448
+ * Called after session validation succeeds. Enables generic session functions. */
449
+ resolveUser?: (user: SessionUser) => Promise<SessionUser> | SessionUser;
463
450
  /** Base URL for magic links and SWA preview URL validation */
464
451
  appBaseUrl?: string;
465
452
  /** Required callback to send magic link emails */
@@ -480,16 +467,28 @@ export declare function initSessionAuth(config: SessionAuthConfig): void;
480
467
  /**
481
468
  * Initializes session auth from environment variables.
482
469
  * Reads: SESSION_COOKIE_NAME, APP_BASE_URL, ALLOWED_EMAILS, ALLOWED_DOMAIN, ADMIN_EMAILS.
483
- * @param sendEmail - Required callback to send magic link emails
484
- * @param overrides - Optional config overrides (e.g., isEmailAllowed callback)
470
+ * Only callbacks go in the config object data comes from env vars.
471
+ * Use initSessionAuth() directly for full control.
472
+ * @param config - Required config with sendEmail callback and optional isEmailAllowed
485
473
  * @throws Error if sendEmail is not provided
486
474
  * @example
487
- * initSessionAuthFromEnv(async (to, magicLink) => {
488
- * await resend.emails.send({ to, html: `<a href="${magicLink}">Sign In</a>` });
489
- * return true;
490
- * }, { isEmailAllowed: async (email) => lookupInDatabase(email) });
475
+ * initSessionAuthFromEnv({
476
+ * sendEmail: async (to, magicLink) => {
477
+ * await resend.emails.send({ to, html: `<a href="${magicLink}">Sign In</a>` });
478
+ * return true;
479
+ * },
480
+ * isEmailAllowed: async (email) => lookupInDatabase(email),
481
+ * });
491
482
  */
492
- export declare function initSessionAuthFromEnv(sendEmail: (to: string, magicLink: string) => Promise<boolean>, overrides?: Partial<Omit<SessionAuthConfig, 'sendEmail'>>): void;
483
+ export declare function initSessionAuthFromEnv(config: {
484
+ sendEmail: (to: string, magicLink: string) => Promise<boolean>;
485
+ /** Additional email validation. Called after built-in domain/admin checks reject.
486
+ * Can only widen access, never narrow it. */
487
+ isEmailAllowed?: (email: string) => boolean | Promise<boolean>;
488
+ /** Transform base SessionUser into an app-specific user type.
489
+ * Called after session validation succeeds. Enables generic session functions. */
490
+ resolveUser?: (user: SessionUser) => Promise<SessionUser> | SessionUser;
491
+ }): void;
493
492
  /**
494
493
  * Parses cookies from a request's Cookie header.
495
494
  * @param request - Request object with headers.get() method
@@ -582,37 +581,37 @@ export declare function verifyMagicLink(token: string): Promise<Result<{
582
581
  * Validates a session cookie and returns the user and session info.
583
582
  * Performs sliding window refresh if the session is close to expiry.
584
583
  * @param request - Request object with headers.get() method
585
- * @returns User, session, and optional refreshed cookie, or null if invalid
584
+ * @returns Result with user, session, and optional refreshed cookie
586
585
  * @example
587
586
  * const result = await validateSession(request);
588
- * if (!result) return unauthorizedResponse();
587
+ * if (!result.ok) return unauthorizedResponse();
589
588
  */
590
- export declare function validateSession(request: {
589
+ export declare function validateSession<T extends SessionUser = SessionUser>(request: {
591
590
  headers: {
592
591
  get(name: string): string | null;
593
592
  };
594
- }): Promise<{
595
- user: SessionUser;
593
+ }): Promise<Result<{
594
+ user: T;
596
595
  session: SessionInfo;
597
596
  refreshedCookie?: string;
598
- } | null>;
597
+ }>>;
599
598
  /**
600
599
  * Convenience function: validates session and returns user with optional refresh headers.
601
600
  * @param request - Request object with headers.get() method
602
- * @returns User and optional Set-Cookie headers, or null if not authenticated
601
+ * @returns Result with user and optional Set-Cookie headers
603
602
  * @example
604
603
  * const result = await getSessionUser(request);
605
- * if (!result) return unauthorizedResponse();
606
- * return { headers: result.headers, jsonBody: { user: result.user } };
604
+ * if (!result.ok) return resultToResponse(result);
605
+ * const { user, headers } = result.data!;
607
606
  */
608
- export declare function getSessionUser(request: {
607
+ export declare function getSessionUser<T extends SessionUser = SessionUser>(request: {
609
608
  headers: {
610
609
  get(name: string): string | null;
611
610
  };
612
- }): Promise<{
613
- user: SessionUser;
611
+ }): Promise<Result<{
612
+ user: T;
614
613
  headers?: Record<string, string>;
615
- } | null>;
614
+ }>>;
616
615
  /**
617
616
  * Destroys the current session and returns a logout cookie string.
618
617
  * @param request - Request object with headers.get() method
@@ -638,8 +637,8 @@ export declare function destroySession(request: {
638
637
  * })
639
638
  * });
640
639
  */
641
- export declare function withSessionAuth(handler: (request: HttpRequest, context: InvocationContext, auth: {
642
- user: SessionUser;
640
+ export declare function withSessionAuth<T extends SessionUser = SessionUser>(handler: (request: HttpRequest, context: InvocationContext, auth: {
641
+ user: T;
643
642
  headers?: Record<string, string>;
644
643
  }) => Promise<HttpResponseInit>): (request: HttpRequest, context: InvocationContext) => Promise<HttpResponseInit>;
645
644
  /**
@@ -654,8 +653,8 @@ export declare function withSessionAuth(handler: (request: HttpRequest, context:
654
653
  * })
655
654
  * });
656
655
  */
657
- export declare function withSessionAdminAuth(handler: (request: HttpRequest, context: InvocationContext, auth: {
658
- user: SessionUser;
656
+ export declare function withSessionAdminAuth<T extends SessionUser = SessionUser>(handler: (request: HttpRequest, context: InvocationContext, auth: {
657
+ user: T;
659
658
  headers?: Record<string, string>;
660
659
  }) => Promise<HttpResponseInit>): (request: HttpRequest, context: InvocationContext) => Promise<HttpResponseInit>;
661
660
  /**
@@ -703,4 +702,14 @@ export declare function hasKeyVaultReferences(): boolean;
703
702
  * }
704
703
  */
705
704
  export declare function resolveKeyVaultReferences(): Promise<number>;
705
+ /**
706
+ * Convert a failed Result to an HttpResponseInit.
707
+ * Use after checking `!result.ok` to return the error as an HTTP response.
708
+ * @param result - A failed Result (ok=false)
709
+ * @returns HttpResponseInit with the error status and message
710
+ * @example
711
+ * const auth = requireAuth<UsernameTokenPayload>(authHeader);
712
+ * if (!auth.ok) return resultToResponse(auth);
713
+ */
714
+ export declare function resultToResponse(result: Result<unknown>): HttpResponseInit;
706
715
  export { Result, ok, okVoid, err, getErrorMessage, BaseJwtPayload, UserTokenPayload, UsernameTokenPayload, RoleTokenPayload, hasUsername, hasRole, isAdmin, HTTP_STATUS, HttpStatus, ErrorResponse, SessionUser, SessionInfo, MagicLinkRequest, SessionAuthResponse } from './shared';
package/dist/server.js CHANGED
@@ -101,6 +101,7 @@ exports.deleteExpiredSessions = deleteExpiredSessions;
101
101
  exports.deleteExpiredMagicLinks = deleteExpiredMagicLinks;
102
102
  exports.hasKeyVaultReferences = hasKeyVaultReferences;
103
103
  exports.resolveKeyVaultReferences = resolveKeyVaultReferences;
104
+ exports.resultToResponse = resultToResponse;
104
105
  const crypto_1 = require("crypto");
105
106
  const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
106
107
  const data_tables_1 = require("@azure/data-tables");
@@ -130,15 +131,13 @@ function initAuth(config) {
130
131
  /**
131
132
  * Initializes JWT authentication from environment variables.
132
133
  * Reads JWT_SECRET from process.env.
133
- * @param minLength - Minimum required secret length (default: 32)
134
134
  * @throws Error if JWT_SECRET is missing or too short
135
135
  * @example
136
136
  * initAuthFromEnv(); // Uses process.env.JWT_SECRET
137
137
  */
138
- function initAuthFromEnv(minLength = 32) {
138
+ function initAuthFromEnv() {
139
139
  initAuth({
140
140
  secret: process.env.JWT_SECRET,
141
- minLength
142
141
  });
143
142
  }
144
143
  /**
@@ -225,41 +224,41 @@ exports.DEFAULT_TOKEN_EXPIRY_SECONDS = DEFAULT_TOKEN_EXPIRY_DAYS * 24 * 60 * 60;
225
224
  // Auth Helpers
226
225
  // =============================================================================
227
226
  /**
228
- * Validates auth header and returns typed payload or error response.
227
+ * Validates auth header and returns typed payload or error result.
229
228
  * @typeParam T - The expected payload type (extends BaseJwtPayload)
230
229
  * @param authHeader - The Authorization header value
231
- * @returns AuthResult with payload on success, or HTTP response on failure
230
+ * @returns Result with payload on success, or error with status on failure
232
231
  * @example
233
232
  * const auth = requireAuth<UsernameTokenPayload>(request.headers.get('Authorization'));
234
- * if (!auth.authorized) return auth.response;
235
- * console.log(auth.payload.username);
233
+ * if (!auth.ok) return resultToResponse(auth);
234
+ * console.log(auth.data.username);
236
235
  */
237
236
  function requireAuth(authHeader) {
238
237
  const token = extractToken(authHeader);
239
238
  if (!token) {
240
- return { authorized: false, response: unauthorizedResponse() };
239
+ return (0, shared_1.err)('Unauthorized', shared_1.HTTP_STATUS.UNAUTHORIZED);
241
240
  }
242
241
  const result = validateToken(token);
243
242
  if (!result.ok) {
244
- return { authorized: false, response: unauthorizedResponse(result.error) };
243
+ return (0, shared_1.err)(result.error ?? 'Unauthorized', shared_1.HTTP_STATUS.UNAUTHORIZED);
245
244
  }
246
- return { authorized: true, payload: result.data };
245
+ return (0, shared_1.ok)(result.data);
247
246
  }
248
247
  /**
249
- * Requires admin role. Use with RoleTokenPayload.
248
+ * Requires admin role. Generic like requireAuth — callers can extend RoleTokenPayload.
250
249
  * @param authHeader - The Authorization header value
251
- * @returns AuthResult with RoleTokenPayload on success, or HTTP response on failure
250
+ * @returns Result with payload on success, or error with status on failure
252
251
  * @example
253
252
  * const auth = requireAdmin(request.headers.get('Authorization'));
254
- * if (!auth.authorized) return auth.response;
253
+ * if (!auth.ok) return resultToResponse(auth);
255
254
  * // User is admin
256
255
  */
257
256
  function requireAdmin(authHeader) {
258
257
  const auth = requireAuth(authHeader);
259
- if (!auth.authorized)
258
+ if (!auth.ok)
260
259
  return auth;
261
- if (!(0, shared_1.isAdmin)(auth.payload)) {
262
- return { authorized: false, response: forbiddenResponse('Admin access required') };
260
+ if (!(0, shared_1.isAdmin)(auth.data)) {
261
+ return (0, shared_1.err)('Admin access required', shared_1.HTTP_STATUS.FORBIDDEN);
263
262
  }
264
263
  return auth;
265
264
  }
@@ -459,14 +458,14 @@ function initErrorHandling(config = {}) {
459
458
  * Initializes error handling from environment variables.
460
459
  * Currently a no-op but provides consistent API for future error config
461
460
  * (e.g., ERROR_LOG_LEVEL, ERROR_INCLUDE_STACK).
462
- * @param callback - Optional callback invoked when handleFunctionError is called (fire-and-forget)
461
+ * @param config - Optional config with onError callback invoked when handleFunctionError is called (fire-and-forget)
463
462
  * @example
464
463
  * initErrorHandlingFromEnv(); // Uses process.env automatically
465
- * initErrorHandlingFromEnv((op, msg) => sendAlert(op, msg));
464
+ * initErrorHandlingFromEnv({ onError: (op, msg) => sendAlert(op, msg) });
466
465
  */
467
- function initErrorHandlingFromEnv(callback) {
466
+ function initErrorHandlingFromEnv(config) {
468
467
  // Future: read ERROR_LOG_LEVEL, ERROR_INCLUDE_STACK, etc. from process.env
469
- initErrorHandling({ callback });
468
+ initErrorHandling({ callback: config?.onError });
470
469
  }
471
470
  /**
472
471
  * Handles unexpected errors safely by logging details and returning a generic message.
@@ -695,21 +694,25 @@ async function upsertEntity(client, entity) {
695
694
  }
696
695
  /**
697
696
  * Deletes an entity from Azure Table Storage.
698
- * Returns true on success, false on error (swallows errors).
697
+ * Returns ok(true) if deleted, ok(false) if not found (not an error), err() on real failures.
699
698
  * @param client - The TableClient instance
700
699
  * @param partitionKey - The partition key
701
700
  * @param rowKey - The row key
702
- * @returns True if deleted successfully, false on error
701
+ * @returns Result with true if deleted, false if not found, or error on failure
703
702
  * @example
704
- * const deleted = await deleteEntity(client, 'session', sessionId);
703
+ * const result = await deleteEntity(client, 'session', sessionId);
704
+ * if (!result.ok) console.error(result.error);
705
705
  */
706
706
  async function deleteEntity(client, partitionKey, rowKey) {
707
707
  try {
708
708
  await client.deleteEntity(partitionKey, rowKey);
709
- return true;
709
+ return (0, shared_1.ok)(true);
710
710
  }
711
- catch {
712
- return false;
711
+ catch (error) {
712
+ if (isNotFoundError(error)) {
713
+ return (0, shared_1.ok)(false);
714
+ }
715
+ return (0, shared_1.err)((0, shared_1.getErrorMessage)(error, 'Delete failed'));
713
716
  }
714
717
  }
715
718
  /**
@@ -788,16 +791,20 @@ function initSessionAuth(config) {
788
791
  /**
789
792
  * Initializes session auth from environment variables.
790
793
  * Reads: SESSION_COOKIE_NAME, APP_BASE_URL, ALLOWED_EMAILS, ALLOWED_DOMAIN, ADMIN_EMAILS.
791
- * @param sendEmail - Required callback to send magic link emails
792
- * @param overrides - Optional config overrides (e.g., isEmailAllowed callback)
794
+ * Only callbacks go in the config object data comes from env vars.
795
+ * Use initSessionAuth() directly for full control.
796
+ * @param config - Required config with sendEmail callback and optional isEmailAllowed
793
797
  * @throws Error if sendEmail is not provided
794
798
  * @example
795
- * initSessionAuthFromEnv(async (to, magicLink) => {
796
- * await resend.emails.send({ to, html: `<a href="${magicLink}">Sign In</a>` });
797
- * return true;
798
- * }, { isEmailAllowed: async (email) => lookupInDatabase(email) });
799
+ * initSessionAuthFromEnv({
800
+ * sendEmail: async (to, magicLink) => {
801
+ * await resend.emails.send({ to, html: `<a href="${magicLink}">Sign In</a>` });
802
+ * return true;
803
+ * },
804
+ * isEmailAllowed: async (email) => lookupInDatabase(email),
805
+ * });
799
806
  */
800
- function initSessionAuthFromEnv(sendEmail, overrides) {
807
+ function initSessionAuthFromEnv(config) {
801
808
  const allowedEmailsStr = process.env.ALLOWED_EMAILS;
802
809
  const adminEmailsStr = process.env.ADMIN_EMAILS;
803
810
  initSessionAuth({
@@ -810,8 +817,9 @@ function initSessionAuthFromEnv(sendEmail, overrides) {
810
817
  adminEmails: adminEmailsStr
811
818
  ? adminEmailsStr.split(',').map(e => e.trim().toLowerCase())
812
819
  : undefined,
813
- ...overrides,
814
- sendEmail
820
+ isEmailAllowed: config.isEmailAllowed,
821
+ resolveUser: config.resolveUser,
822
+ sendEmail: config.sendEmail,
815
823
  });
816
824
  }
817
825
  /**
@@ -1007,10 +1015,11 @@ async function createMagicLink(email, request) {
1007
1015
  if (!isValidEmail(normalizedEmail)) {
1008
1016
  return (0, shared_1.err)('Valid email required', shared_1.HTTP_STATUS.BAD_REQUEST);
1009
1017
  }
1010
- // Check allowlist (custom callback overrides default)
1011
- const emailAllowed = config.isEmailAllowed
1012
- ? await config.isEmailAllowed(normalizedEmail)
1013
- : isEmailAllowed(normalizedEmail);
1018
+ // Check allowlist (built-in first, custom extends can only widen access)
1019
+ const emailAllowed = isEmailAllowed(normalizedEmail)
1020
+ || (config.isEmailAllowed
1021
+ ? await config.isEmailAllowed(normalizedEmail)
1022
+ : false);
1014
1023
  if (!emailAllowed) {
1015
1024
  return (0, shared_1.err)('Email not allowed', shared_1.HTTP_STATUS.FORBIDDEN);
1016
1025
  }
@@ -1123,10 +1132,10 @@ async function verifyMagicLink(token) {
1123
1132
  * Validates a session cookie and returns the user and session info.
1124
1133
  * Performs sliding window refresh if the session is close to expiry.
1125
1134
  * @param request - Request object with headers.get() method
1126
- * @returns User, session, and optional refreshed cookie, or null if invalid
1135
+ * @returns Result with user, session, and optional refreshed cookie
1127
1136
  * @example
1128
1137
  * const result = await validateSession(request);
1129
- * if (!result) return unauthorizedResponse();
1138
+ * if (!result.ok) return unauthorizedResponse();
1130
1139
  */
1131
1140
  async function validateSession(request) {
1132
1141
  const config = getSessionAuthConfig();
@@ -1134,27 +1143,31 @@ async function validateSession(request) {
1134
1143
  const cookies = parseCookies(request);
1135
1144
  const sessionId = cookies[cookieName];
1136
1145
  if (!sessionId)
1137
- return null;
1146
+ return (0, shared_1.err)('No session cookie', shared_1.HTTP_STATUS.UNAUTHORIZED);
1138
1147
  try {
1139
1148
  const sessionsClient = await getTableClient(SESSIONS_TABLE);
1140
1149
  const sessionEntity = await getEntityIfExists(sessionsClient, SESSION_PARTITION, sessionId);
1141
1150
  if (!sessionEntity)
1142
- return null;
1151
+ return (0, shared_1.err)('Invalid session', shared_1.HTTP_STATUS.UNAUTHORIZED);
1143
1152
  // Check expiry
1144
1153
  if (new Date(sessionEntity.expiresAt) < new Date())
1145
- return null;
1154
+ return (0, shared_1.err)('Session expired', shared_1.HTTP_STATUS.UNAUTHORIZED);
1146
1155
  // Get user
1147
1156
  const usersClient = await getTableClient(USERS_TABLE);
1148
1157
  const userEntity = await getEntityIfExists(usersClient, USER_PARTITION, sessionEntity.email.toLowerCase());
1149
1158
  if (!userEntity)
1150
- return null;
1151
- const user = {
1159
+ return (0, shared_1.err)('User not found', shared_1.HTTP_STATUS.UNAUTHORIZED);
1160
+ const baseUser = {
1152
1161
  id: userEntity.id,
1153
1162
  email: userEntity.email,
1154
1163
  isAdmin: userEntity.isAdmin,
1155
1164
  createdAt: userEntity.createdAt,
1156
1165
  lastLoginAt: userEntity.lastLoginAt
1157
1166
  };
1167
+ // Resolve to app-specific user type if callback is configured
1168
+ const user = (config.resolveUser
1169
+ ? await config.resolveUser(baseUser)
1170
+ : baseUser);
1158
1171
  const session = {
1159
1172
  id: sessionEntity.id,
1160
1173
  userId: sessionEntity.userId,
@@ -1179,30 +1192,30 @@ async function validateSession(request) {
1179
1192
  await upsertEntity(sessionsClient, updatedSession);
1180
1193
  refreshedCookie = createSessionCookie(sessionId);
1181
1194
  }
1182
- return { user, session, refreshedCookie };
1195
+ return (0, shared_1.ok)({ user, session, refreshedCookie });
1183
1196
  }
1184
1197
  catch {
1185
- return null;
1198
+ return (0, shared_1.err)('Session validation failed', shared_1.HTTP_STATUS.UNAUTHORIZED);
1186
1199
  }
1187
1200
  }
1188
1201
  // --- Session Operations ---
1189
1202
  /**
1190
1203
  * Convenience function: validates session and returns user with optional refresh headers.
1191
1204
  * @param request - Request object with headers.get() method
1192
- * @returns User and optional Set-Cookie headers, or null if not authenticated
1205
+ * @returns Result with user and optional Set-Cookie headers
1193
1206
  * @example
1194
1207
  * const result = await getSessionUser(request);
1195
- * if (!result) return unauthorizedResponse();
1196
- * return { headers: result.headers, jsonBody: { user: result.user } };
1208
+ * if (!result.ok) return resultToResponse(result);
1209
+ * const { user, headers } = result.data!;
1197
1210
  */
1198
1211
  async function getSessionUser(request) {
1199
1212
  const result = await validateSession(request);
1200
- if (!result)
1201
- return null;
1202
- const headers = result.refreshedCookie
1203
- ? { 'Set-Cookie': result.refreshedCookie }
1213
+ if (!result.ok)
1214
+ return result;
1215
+ const headers = result.data.refreshedCookie
1216
+ ? { 'Set-Cookie': result.data.refreshedCookie }
1204
1217
  : undefined;
1205
- return { user: result.user, headers };
1218
+ return (0, shared_1.ok)({ user: result.data.user, headers });
1206
1219
  }
1207
1220
  /**
1208
1221
  * Destroys the current session and returns a logout cookie string.
@@ -1239,13 +1252,13 @@ async function destroySession(request) {
1239
1252
  function withSessionAuth(handler) {
1240
1253
  return async (request, context) => {
1241
1254
  const result = await getSessionUser(request);
1242
- if (!result) {
1255
+ if (!result.ok) {
1243
1256
  return unauthorizedResponse('Not authenticated');
1244
1257
  }
1245
- const response = await handler(request, context, result);
1258
+ const response = await handler(request, context, result.data);
1246
1259
  // Merge refresh cookie headers
1247
- if (result.headers) {
1248
- response.headers = { ...result.headers, ...response.headers };
1260
+ if (result.data.headers) {
1261
+ response.headers = { ...result.data.headers, ...response.headers };
1249
1262
  }
1250
1263
  return response;
1251
1264
  };
@@ -1265,16 +1278,16 @@ function withSessionAuth(handler) {
1265
1278
  function withSessionAdminAuth(handler) {
1266
1279
  return async (request, context) => {
1267
1280
  const result = await getSessionUser(request);
1268
- if (!result) {
1281
+ if (!result.ok) {
1269
1282
  return unauthorizedResponse('Not authenticated');
1270
1283
  }
1271
- if (!result.user.isAdmin) {
1284
+ if (!result.data.user.isAdmin) {
1272
1285
  return forbiddenResponse('Admin access required');
1273
1286
  }
1274
- const response = await handler(request, context, result);
1287
+ const response = await handler(request, context, result.data);
1275
1288
  // Merge refresh cookie headers
1276
- if (result.headers) {
1277
- response.headers = { ...result.headers, ...response.headers };
1289
+ if (result.data.headers) {
1290
+ response.headers = { ...result.data.headers, ...response.headers };
1278
1291
  }
1279
1292
  return response;
1280
1293
  };
@@ -1295,8 +1308,8 @@ async function deleteExpiredSessions() {
1295
1308
  let deleted = 0;
1296
1309
  for (const session of sessions) {
1297
1310
  if (new Date(session.expiresAt) < now) {
1298
- const success = await deleteEntity(client, SESSION_PARTITION, session.rowKey);
1299
- if (success)
1311
+ const result = await deleteEntity(client, SESSION_PARTITION, session.rowKey);
1312
+ if (result.ok && result.data)
1300
1313
  deleted++;
1301
1314
  }
1302
1315
  }
@@ -1317,8 +1330,8 @@ async function deleteExpiredMagicLinks() {
1317
1330
  let deleted = 0;
1318
1331
  for (const link of links) {
1319
1332
  if (new Date(link.expiresAt) < now || link.used) {
1320
- const success = await deleteEntity(client, MAGICLINK_PARTITION, link.rowKey);
1321
- if (success)
1333
+ const result = await deleteEntity(client, MAGICLINK_PARTITION, link.rowKey);
1334
+ if (result.ok && result.data)
1322
1335
  deleted++;
1323
1336
  }
1324
1337
  }
@@ -1432,6 +1445,24 @@ async function resolveKeyVaultReferences() {
1432
1445
  return resolved;
1433
1446
  }
1434
1447
  // =============================================================================
1448
+ // Result Conversion Helper
1449
+ // =============================================================================
1450
+ /**
1451
+ * Convert a failed Result to an HttpResponseInit.
1452
+ * Use after checking `!result.ok` to return the error as an HTTP response.
1453
+ * @param result - A failed Result (ok=false)
1454
+ * @returns HttpResponseInit with the error status and message
1455
+ * @example
1456
+ * const auth = requireAuth<UsernameTokenPayload>(authHeader);
1457
+ * if (!auth.ok) return resultToResponse(auth);
1458
+ */
1459
+ function resultToResponse(result) {
1460
+ return {
1461
+ status: result.status ?? shared_1.HTTP_STATUS.INTERNAL_ERROR,
1462
+ jsonBody: { error: result.error ?? 'Unknown error' }
1463
+ };
1464
+ }
1465
+ // =============================================================================
1435
1466
  // Re-exports from shared (for convenience)
1436
1467
  // =============================================================================
1437
1468
  var shared_2 = require("./shared");
package/dist/shared.d.ts CHANGED
@@ -15,13 +15,13 @@
15
15
  * { ok: false, error: 'Token expired' }
16
16
  *
17
17
  * // With status code (HTTP/push operations)
18
- * { ok: false, error: 'Subscription expired', statusCode: 410 }
18
+ * { ok: false, error: 'Subscription expired', status: 410 }
19
19
  */
20
20
  export interface Result<T> {
21
21
  ok: boolean;
22
22
  data?: T;
23
23
  error?: string;
24
- statusCode?: number;
24
+ status?: number;
25
25
  }
26
26
  /**
27
27
  * Creates a success result with data.
@@ -41,13 +41,13 @@ export declare function okVoid(): Result<void>;
41
41
  /**
42
42
  * Creates a failure result with an error message.
43
43
  * @param error - The error message
44
- * @param statusCode - Optional HTTP status code for API/push operations
44
+ * @param status - Optional HTTP status code for API/push operations
45
45
  * @returns A Result with ok=false and the error details
46
46
  * @example
47
47
  * return err('Token expired');
48
48
  * return err('Subscription gone', 410);
49
49
  */
50
- export declare function err<T>(error: string, statusCode?: number): Result<T>;
50
+ export declare function err<T = never>(error: string, status?: number): Result<T>;
51
51
  /**
52
52
  * Base JWT payload - all tokens include these fields
53
53
  * Projects extend this with their specific fields