@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/client.d.ts +47 -89
- package/dist/client.js +102 -252
- package/dist/server.d.ts +71 -62
- package/dist/server.js +100 -69
- package/dist/shared.d.ts +4 -4
- package/dist/shared.js +4 -4
- package/package.json +1 -1
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(
|
|
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
|
|
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
|
|
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.
|
|
109
|
-
* console.log(auth.
|
|
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):
|
|
91
|
+
export declare function requireAuth<T extends BaseJwtPayload>(authHeader: string | null): Result<T>;
|
|
112
92
|
/**
|
|
113
|
-
* Requires admin role.
|
|
93
|
+
* Requires admin role. Generic like requireAuth — callers can extend RoleTokenPayload.
|
|
114
94
|
* @param authHeader - The Authorization header value
|
|
115
|
-
* @returns
|
|
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.
|
|
98
|
+
* if (!auth.ok) return resultToResponse(auth);
|
|
119
99
|
* // User is admin
|
|
120
100
|
*/
|
|
121
|
-
export declare function requireAdmin(authHeader: string | null):
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
386
|
+
* @returns Result with true if deleted, false if not found, or error on failure
|
|
405
387
|
* @example
|
|
406
|
-
* const
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
484
|
-
*
|
|
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(
|
|
488
|
-
*
|
|
489
|
-
*
|
|
490
|
-
*
|
|
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(
|
|
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
|
|
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:
|
|
593
|
+
}): Promise<Result<{
|
|
594
|
+
user: T;
|
|
596
595
|
session: SessionInfo;
|
|
597
596
|
refreshedCookie?: string;
|
|
598
|
-
}
|
|
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
|
|
601
|
+
* @returns Result with user and optional Set-Cookie headers
|
|
603
602
|
* @example
|
|
604
603
|
* const result = await getSessionUser(request);
|
|
605
|
-
* if (!result) return
|
|
606
|
-
*
|
|
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:
|
|
611
|
+
}): Promise<Result<{
|
|
612
|
+
user: T;
|
|
614
613
|
headers?: Record<string, string>;
|
|
615
|
-
}
|
|
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:
|
|
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:
|
|
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(
|
|
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
|
|
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
|
|
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.
|
|
235
|
-
* console.log(auth.
|
|
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
|
|
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
|
|
243
|
+
return (0, shared_1.err)(result.error ?? 'Unauthorized', shared_1.HTTP_STATUS.UNAUTHORIZED);
|
|
245
244
|
}
|
|
246
|
-
return
|
|
245
|
+
return (0, shared_1.ok)(result.data);
|
|
247
246
|
}
|
|
248
247
|
/**
|
|
249
|
-
* Requires admin role.
|
|
248
|
+
* Requires admin role. Generic like requireAuth — callers can extend RoleTokenPayload.
|
|
250
249
|
* @param authHeader - The Authorization header value
|
|
251
|
-
* @returns
|
|
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.
|
|
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.
|
|
258
|
+
if (!auth.ok)
|
|
260
259
|
return auth;
|
|
261
|
-
if (!(0, shared_1.isAdmin)(auth.
|
|
262
|
-
return
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
701
|
+
* @returns Result with true if deleted, false if not found, or error on failure
|
|
703
702
|
* @example
|
|
704
|
-
* const
|
|
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
|
-
|
|
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
|
-
*
|
|
792
|
-
*
|
|
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(
|
|
796
|
-
*
|
|
797
|
-
*
|
|
798
|
-
*
|
|
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(
|
|
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
|
-
|
|
814
|
-
|
|
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
|
|
1011
|
-
const emailAllowed =
|
|
1012
|
-
|
|
1013
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1151
|
-
const
|
|
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
|
|
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
|
|
1205
|
+
* @returns Result with user and optional Set-Cookie headers
|
|
1193
1206
|
* @example
|
|
1194
1207
|
* const result = await getSessionUser(request);
|
|
1195
|
-
* if (!result) return
|
|
1196
|
-
*
|
|
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
|
|
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
|
|
1299
|
-
if (
|
|
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
|
|
1321
|
-
if (
|
|
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',
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|