@markwharton/pwa-core 3.3.0 → 3.4.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/server.d.ts CHANGED
@@ -3,9 +3,9 @@
3
3
  *
4
4
  * Includes: JWT auth, API keys, HTTP responses, Azure Table Storage
5
5
  */
6
- import { HttpResponseInit, InvocationContext } from '@azure/functions';
6
+ import { HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions';
7
7
  import { TableClient } from '@azure/data-tables';
8
- import { Result, BaseJwtPayload, RoleTokenPayload } from './shared';
8
+ import { Result, BaseJwtPayload, RoleTokenPayload, SessionUser, SessionInfo } from './shared';
9
9
  /**
10
10
  * Initializes the JWT authentication system. Call once at application startup.
11
11
  * @param config - Configuration object
@@ -200,6 +200,30 @@ export declare function notFoundResponse(resource: string): HttpResponseInit;
200
200
  * if (existingUser) return conflictResponse('Email already registered');
201
201
  */
202
202
  export declare function conflictResponse(message: string): HttpResponseInit;
203
+ /**
204
+ * Creates a 410 Gone response.
205
+ * @param message - The error message to return
206
+ * @returns Azure Functions HttpResponseInit object
207
+ * @example
208
+ * if (linkUsed) return goneResponse('Link already used');
209
+ */
210
+ export declare function goneResponse(message: string): HttpResponseInit;
211
+ /**
212
+ * Creates a 429 Too Many Requests response.
213
+ * @param message - The error message to return
214
+ * @returns Azure Functions HttpResponseInit object
215
+ * @example
216
+ * if (rateLimited) return tooManyRequestsResponse('Too many requests');
217
+ */
218
+ export declare function tooManyRequestsResponse(message: string): HttpResponseInit;
219
+ /**
220
+ * Creates a 503 Service Unavailable response.
221
+ * @param message - The error message to return
222
+ * @returns Azure Functions HttpResponseInit object
223
+ * @example
224
+ * if (!emailSent) return serviceUnavailableResponse('Email service unavailable');
225
+ */
226
+ export declare function serviceUnavailableResponse(message: string): HttpResponseInit;
203
227
  /**
204
228
  * Validates that a required parameter is present.
205
229
  * Returns an error response if missing, null if valid.
@@ -359,6 +383,50 @@ export declare function clearTableClientCache(): void;
359
383
  * if (!user) return notFoundResponse('User');
360
384
  */
361
385
  export declare function getEntityIfExists<T extends object>(client: TableClient, partitionKey: string, rowKey: string): Promise<T | null>;
386
+ /**
387
+ * Upserts (insert or replace) an entity in Azure Table Storage.
388
+ * @typeParam T - The entity type with partitionKey and rowKey
389
+ * @param client - The TableClient instance
390
+ * @param entity - The entity to upsert
391
+ * @example
392
+ * await upsertEntity(client, { partitionKey: 'user', rowKey: 'john', name: 'John' });
393
+ */
394
+ export declare function upsertEntity<T extends {
395
+ partitionKey: string;
396
+ rowKey: string;
397
+ }>(client: TableClient, entity: T): Promise<void>;
398
+ /**
399
+ * Deletes an entity from Azure Table Storage.
400
+ * Returns true on success, false on error (swallows errors).
401
+ * @param client - The TableClient instance
402
+ * @param partitionKey - The partition key
403
+ * @param rowKey - The row key
404
+ * @returns True if deleted successfully, false on error
405
+ * @example
406
+ * const deleted = await deleteEntity(client, 'session', sessionId);
407
+ */
408
+ export declare function deleteEntity(client: TableClient, partitionKey: string, rowKey: string): Promise<boolean>;
409
+ /**
410
+ * Lists all entities in a partition.
411
+ * @typeParam T - The entity type
412
+ * @param client - The TableClient instance
413
+ * @param partitionKey - The partition key to filter by
414
+ * @returns Array of entities in the partition
415
+ * @example
416
+ * const sessions = await listEntitiesByPartition<SessionEntity>(client, 'session');
417
+ */
418
+ export declare function listEntitiesByPartition<T extends object>(client: TableClient, partitionKey: string): Promise<T[]>;
419
+ /**
420
+ * Lists entities matching a custom OData filter.
421
+ * @typeParam T - The entity type
422
+ * @param client - The TableClient instance
423
+ * @param filter - OData filter string (use the odata template tag from @azure/data-tables)
424
+ * @returns Array of matching entities
425
+ * @example
426
+ * const filter = odata`PartitionKey eq ${'user'} and email eq ${'john@example.com'}`;
427
+ * const users = await listEntitiesWithFilter<UserEntity>(client, filter);
428
+ */
429
+ export declare function listEntitiesWithFilter<T extends object>(client: TableClient, filter: string): Promise<T[]>;
362
430
  /**
363
431
  * Generate a unique row key from an identifier string.
364
432
  * Uses SHA-256 hash for consistent, URL-safe keys.
@@ -366,4 +434,243 @@ export declare function getEntityIfExists<T extends object>(client: TableClient,
366
434
  * @returns A 32-character hex string suitable for Azure Table Storage row keys
367
435
  */
368
436
  export declare function generateRowKey(identifier: string): string;
369
- export { Result, ok, okVoid, err, getErrorMessage, BaseJwtPayload, UserTokenPayload, UsernameTokenPayload, RoleTokenPayload, hasUsername, hasRole, isAdmin, HTTP_STATUS, HttpStatus, ErrorResponse } from './shared';
437
+ /**
438
+ * Configuration for session-based authentication.
439
+ */
440
+ export interface SessionAuthConfig {
441
+ /** Cookie name for the session (default: 'app_session') */
442
+ cookieName?: string;
443
+ /** Session duration in ms (default: 30 days) */
444
+ sessionDurationMs?: number;
445
+ /** Refresh threshold in ms - refresh session if within this time of expiry (default: 24 hours) */
446
+ sessionRefreshThresholdMs?: number;
447
+ /** Magic link token expiry in ms (default: 15 minutes) */
448
+ magicLinkExpiryMs?: number;
449
+ /** Rate limit window in ms (default: 1 hour) */
450
+ rateLimitWindowMs?: number;
451
+ /** Max magic link requests per email per window (default: 5) */
452
+ rateLimitMaxRequests?: number;
453
+ /** SameSite cookie attribute (default: 'Strict') */
454
+ sameSite?: 'Strict' | 'Lax';
455
+ /** Specific emails allowed (empty = allow all) */
456
+ allowedEmails?: string[];
457
+ /** Domain allowlist suffix (e.g., '@company.com') */
458
+ allowedDomain?: string;
459
+ /** Emails that get isAdmin=true */
460
+ adminEmails?: string[];
461
+ /** Base URL for magic links and SWA preview URL validation */
462
+ appBaseUrl?: string;
463
+ /** Required callback to send magic link emails */
464
+ sendEmail: (to: string, magicLink: string) => Promise<boolean>;
465
+ }
466
+ /**
467
+ * Initializes session-based authentication. Call once at application startup.
468
+ * @param config - Session auth configuration
469
+ * @throws Error if sendEmail callback is not provided
470
+ * @example
471
+ * initSessionAuth({
472
+ * sendEmail: async (to, magicLink) => { await resend.emails.send(...); return true; },
473
+ * allowedEmails: ['admin@example.com'],
474
+ * appBaseUrl: 'https://myapp.com'
475
+ * });
476
+ */
477
+ export declare function initSessionAuth(config: SessionAuthConfig): void;
478
+ /**
479
+ * Initializes session auth from environment variables.
480
+ * Reads: SESSION_COOKIE_NAME, APP_BASE_URL, ALLOWED_EMAILS, ALLOWED_DOMAIN, ADMIN_EMAILS.
481
+ * @param sendEmail - Required callback to send magic link emails
482
+ * @throws Error if sendEmail is not provided
483
+ * @example
484
+ * initSessionAuthFromEnv(async (to, magicLink) => {
485
+ * await resend.emails.send({ to, html: `<a href="${magicLink}">Sign In</a>` });
486
+ * return true;
487
+ * });
488
+ */
489
+ export declare function initSessionAuthFromEnv(sendEmail: (to: string, magicLink: string) => Promise<boolean>): void;
490
+ /**
491
+ * Parses cookies from a request's Cookie header.
492
+ * @param request - Request object with headers.get() method
493
+ * @returns Record of cookie name/value pairs
494
+ * @example
495
+ * const cookies = parseCookies(request);
496
+ * const sessionId = cookies['app_session'];
497
+ */
498
+ export declare function parseCookies(request: {
499
+ headers: {
500
+ get(name: string): string | null;
501
+ };
502
+ }): Record<string, string>;
503
+ /**
504
+ * Creates a Set-Cookie header value for a session cookie.
505
+ * @param sessionId - The session ID to store in the cookie
506
+ * @returns Cookie string for the Set-Cookie header
507
+ * @example
508
+ * return { headers: { 'Set-Cookie': createSessionCookie(sessionId) } };
509
+ */
510
+ export declare function createSessionCookie(sessionId: string): string;
511
+ /**
512
+ * Creates a Set-Cookie header value that clears the session cookie.
513
+ * @returns Cookie string for the Set-Cookie header
514
+ * @example
515
+ * return { headers: { 'Set-Cookie': createLogoutCookie() } };
516
+ */
517
+ export declare function createLogoutCookie(): string;
518
+ /**
519
+ * Validates a request origin for SWA preview/staging deployments.
520
+ * Returns the trusted origin if it matches the configured appBaseUrl:
521
+ * - Exact hostname match (production)
522
+ * - Localhost-to-localhost match (local development)
523
+ * - App name prefix match for SWA preview slots
524
+ * @param origin - The request Origin header value
525
+ * @returns The trusted origin URL, or undefined if not trusted
526
+ * @example
527
+ * const trusted = getTrustedOrigin(request.headers.get('origin'));
528
+ * const baseUrl = trusted ?? config.appBaseUrl;
529
+ */
530
+ export declare function getTrustedOrigin(origin: string | null): string | undefined;
531
+ /**
532
+ * Validates an email address format.
533
+ * @param email - The email address to validate
534
+ * @returns True if the email format is valid
535
+ */
536
+ export declare function isValidEmail(email: string): boolean;
537
+ /**
538
+ * Checks if an email is allowed by the configured allowlist.
539
+ * Checks both allowedEmails (exact match) and allowedDomain (suffix match).
540
+ * If neither is configured, allows all emails.
541
+ * @param email - The email address to check
542
+ * @returns True if the email is allowed
543
+ */
544
+ export declare function isEmailAllowed(email: string): boolean;
545
+ /**
546
+ * Checks if a magic link request is within rate limits.
547
+ * @param email - The email address to check
548
+ * @returns True if within limits, false if rate limited
549
+ */
550
+ export declare function checkMagicLinkRateLimit(email: string): Promise<boolean>;
551
+ /**
552
+ * Creates a magic link token, stores it, and sends an email.
553
+ * @param email - The email address to send the magic link to
554
+ * @param request - Request object (used to resolve SWA preview origin)
555
+ * @returns Result with the token on success, or error with statusCode
556
+ * @example
557
+ * const result = await createMagicLink(email, request);
558
+ * if (!result.ok) return { status: result.statusCode, jsonBody: { error: result.error } };
559
+ */
560
+ export declare function createMagicLink(email: string, request: {
561
+ headers: {
562
+ get(name: string): string | null;
563
+ };
564
+ }): Promise<Result<string>>;
565
+ /**
566
+ * Verifies a magic link token, creates/updates the user, and creates a session.
567
+ * @param token - The magic link token to verify
568
+ * @returns Result with user and sessionCookie on success, or error with statusCode
569
+ * @example
570
+ * const result = await verifyMagicLink(token);
571
+ * if (!result.ok) return { status: result.statusCode, jsonBody: { error: result.error } };
572
+ * return { headers: { 'Set-Cookie': result.data.sessionCookie }, jsonBody: { user: result.data.user } };
573
+ */
574
+ export declare function verifyMagicLink(token: string): Promise<Result<{
575
+ user: SessionUser;
576
+ sessionCookie: string;
577
+ }>>;
578
+ /**
579
+ * Validates a session cookie and returns the user and session info.
580
+ * Performs sliding window refresh if the session is close to expiry.
581
+ * @param request - Request object with headers.get() method
582
+ * @returns User, session, and optional refreshed cookie, or null if invalid
583
+ * @example
584
+ * const result = await validateSession(request);
585
+ * if (!result) return unauthorizedResponse();
586
+ */
587
+ export declare function validateSession(request: {
588
+ headers: {
589
+ get(name: string): string | null;
590
+ };
591
+ }): Promise<{
592
+ user: SessionUser;
593
+ session: SessionInfo;
594
+ refreshedCookie?: string;
595
+ } | null>;
596
+ /**
597
+ * Convenience function: validates session and returns user with optional refresh headers.
598
+ * @param request - Request object with headers.get() method
599
+ * @returns User and optional Set-Cookie headers, or null if not authenticated
600
+ * @example
601
+ * const result = await getSessionUser(request);
602
+ * if (!result) return unauthorizedResponse();
603
+ * return { headers: result.headers, jsonBody: { user: result.user } };
604
+ */
605
+ export declare function getSessionUser(request: {
606
+ headers: {
607
+ get(name: string): string | null;
608
+ };
609
+ }): Promise<{
610
+ user: SessionUser;
611
+ headers?: Record<string, string>;
612
+ } | null>;
613
+ /**
614
+ * Destroys the current session and returns a logout cookie string.
615
+ * @param request - Request object with headers.get() method
616
+ * @returns Cookie string for the Set-Cookie header
617
+ * @example
618
+ * const cookie = await destroySession(request);
619
+ * return { headers: { 'Set-Cookie': cookie }, jsonBody: { success: true } };
620
+ */
621
+ export declare function destroySession(request: {
622
+ headers: {
623
+ get(name: string): string | null;
624
+ };
625
+ }): Promise<string>;
626
+ /**
627
+ * Wraps a handler with session authentication.
628
+ * Returns 401 if not authenticated. Automatically handles session refresh cookies.
629
+ * @param handler - The handler function that receives the authenticated user
630
+ * @returns A wrapped handler function
631
+ * @example
632
+ * app.http('myEndpoint', {
633
+ * handler: withSessionAuth(async (request, context, auth) => {
634
+ * return { jsonBody: { user: auth.user } };
635
+ * })
636
+ * });
637
+ */
638
+ export declare function withSessionAuth(handler: (request: HttpRequest, context: InvocationContext, auth: {
639
+ user: SessionUser;
640
+ headers?: Record<string, string>;
641
+ }) => Promise<HttpResponseInit>): (request: HttpRequest, context: InvocationContext) => Promise<HttpResponseInit>;
642
+ /**
643
+ * Wraps a handler with session admin authentication.
644
+ * Returns 401 if not authenticated, 403 if not admin.
645
+ * @param handler - The handler function that receives the authenticated admin user
646
+ * @returns A wrapped handler function
647
+ * @example
648
+ * app.http('adminEndpoint', {
649
+ * handler: withSessionAdminAuth(async (request, context, auth) => {
650
+ * return { jsonBody: { admin: auth.user.email } };
651
+ * })
652
+ * });
653
+ */
654
+ export declare function withSessionAdminAuth(handler: (request: HttpRequest, context: InvocationContext, auth: {
655
+ user: SessionUser;
656
+ headers?: Record<string, string>;
657
+ }) => Promise<HttpResponseInit>): (request: HttpRequest, context: InvocationContext) => Promise<HttpResponseInit>;
658
+ /**
659
+ * Deletes expired sessions from storage. Call periodically (e.g., timer trigger).
660
+ * @returns Number of sessions deleted
661
+ * @example
662
+ * // Timer trigger
663
+ * const deleted = await deleteExpiredSessions();
664
+ * context.log(`Cleaned up ${deleted} expired sessions`);
665
+ */
666
+ export declare function deleteExpiredSessions(): Promise<number>;
667
+ /**
668
+ * Deletes expired magic links from storage. Call periodically (e.g., timer trigger).
669
+ * @returns Number of magic links deleted
670
+ * @example
671
+ * // Timer trigger
672
+ * const deleted = await deleteExpiredMagicLinks();
673
+ * context.log(`Cleaned up ${deleted} expired magic links`);
674
+ */
675
+ export declare function deleteExpiredMagicLinks(): Promise<number>;
676
+ export { Result, ok, okVoid, err, getErrorMessage, BaseJwtPayload, UserTokenPayload, UsernameTokenPayload, RoleTokenPayload, hasUsername, hasRole, isAdmin, HTTP_STATUS, HttpStatus, ErrorResponse, SessionUser, SessionInfo, MagicLinkRequest, SessionAuthResponse } from './shared';