@markwharton/pwa-core 1.0.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.
@@ -0,0 +1,24 @@
1
+ /**
2
+ * API Key utilities for machine-to-machine authentication
3
+ */
4
+ /**
5
+ * Extract API key from X-API-Key header
6
+ */
7
+ export declare function extractApiKey(request: {
8
+ headers: {
9
+ get(name: string): string | null;
10
+ };
11
+ }): string | null;
12
+ /**
13
+ * Hash an API key for secure storage
14
+ * Store this hash, not the raw key
15
+ */
16
+ export declare function hashApiKey(apiKey: string): string;
17
+ /**
18
+ * Validate API key against stored hash
19
+ */
20
+ export declare function validateApiKey(apiKey: string, storedHash: string): boolean;
21
+ /**
22
+ * Generate a new API key (random 32-byte hex string)
23
+ */
24
+ export declare function generateApiKey(): string;
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.extractApiKey = extractApiKey;
4
+ exports.hashApiKey = hashApiKey;
5
+ exports.validateApiKey = validateApiKey;
6
+ exports.generateApiKey = generateApiKey;
7
+ const crypto_1 = require("crypto");
8
+ /**
9
+ * API Key utilities for machine-to-machine authentication
10
+ */
11
+ /**
12
+ * Extract API key from X-API-Key header
13
+ */
14
+ function extractApiKey(request) {
15
+ return request.headers.get('X-API-Key');
16
+ }
17
+ /**
18
+ * Hash an API key for secure storage
19
+ * Store this hash, not the raw key
20
+ */
21
+ function hashApiKey(apiKey) {
22
+ return (0, crypto_1.createHash)('sha256').update(apiKey).digest('hex');
23
+ }
24
+ /**
25
+ * Validate API key against stored hash
26
+ */
27
+ function validateApiKey(apiKey, storedHash) {
28
+ const keyHash = hashApiKey(apiKey);
29
+ return keyHash === storedHash;
30
+ }
31
+ /**
32
+ * Generate a new API key (random 32-byte hex string)
33
+ */
34
+ function generateApiKey() {
35
+ return (0, crypto_1.randomBytes)(32).toString('hex');
36
+ }
@@ -0,0 +1,3 @@
1
+ export { type BaseJwtPayload, type UserTokenPayload, type UsernameTokenPayload, type RoleTokenPayload, hasUsername, hasRole, isAdmin } from './types';
2
+ export { initAuth, getJwtSecret, extractToken, validateToken, generateToken, generateLongLivedToken } from './token';
3
+ export { extractApiKey, hashApiKey, validateApiKey, generateApiKey } from './apiKey';
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateApiKey = exports.validateApiKey = exports.hashApiKey = exports.extractApiKey = exports.generateLongLivedToken = exports.generateToken = exports.validateToken = exports.extractToken = exports.getJwtSecret = exports.initAuth = exports.isAdmin = exports.hasRole = exports.hasUsername = void 0;
4
+ // Types and type guards
5
+ var types_1 = require("./types");
6
+ Object.defineProperty(exports, "hasUsername", { enumerable: true, get: function () { return types_1.hasUsername; } });
7
+ Object.defineProperty(exports, "hasRole", { enumerable: true, get: function () { return types_1.hasRole; } });
8
+ Object.defineProperty(exports, "isAdmin", { enumerable: true, get: function () { return types_1.isAdmin; } });
9
+ // JWT functions
10
+ var token_1 = require("./token");
11
+ Object.defineProperty(exports, "initAuth", { enumerable: true, get: function () { return token_1.initAuth; } });
12
+ Object.defineProperty(exports, "getJwtSecret", { enumerable: true, get: function () { return token_1.getJwtSecret; } });
13
+ Object.defineProperty(exports, "extractToken", { enumerable: true, get: function () { return token_1.extractToken; } });
14
+ Object.defineProperty(exports, "validateToken", { enumerable: true, get: function () { return token_1.validateToken; } });
15
+ Object.defineProperty(exports, "generateToken", { enumerable: true, get: function () { return token_1.generateToken; } });
16
+ Object.defineProperty(exports, "generateLongLivedToken", { enumerable: true, get: function () { return token_1.generateLongLivedToken; } });
17
+ // API key functions
18
+ var apiKey_1 = require("./apiKey");
19
+ Object.defineProperty(exports, "extractApiKey", { enumerable: true, get: function () { return apiKey_1.extractApiKey; } });
20
+ Object.defineProperty(exports, "hashApiKey", { enumerable: true, get: function () { return apiKey_1.hashApiKey; } });
21
+ Object.defineProperty(exports, "validateApiKey", { enumerable: true, get: function () { return apiKey_1.validateApiKey; } });
22
+ Object.defineProperty(exports, "generateApiKey", { enumerable: true, get: function () { return apiKey_1.generateApiKey; } });
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Initialize JWT secret - call once at startup
3
+ * Fails fast if secret is missing or too short
4
+ */
5
+ export declare function initAuth(secret: string | undefined, minLength?: number): void;
6
+ /**
7
+ * Get the JWT secret (throws if not initialized)
8
+ */
9
+ export declare function getJwtSecret(): string;
10
+ /**
11
+ * Extract Bearer token from Authorization header
12
+ */
13
+ export declare function extractToken(authHeader: string | null): string | null;
14
+ /**
15
+ * Validate and decode a JWT token
16
+ * Generic type allows project-specific payload shapes
17
+ */
18
+ export declare function validateToken<T extends object>(token: string): T | null;
19
+ /**
20
+ * Generate a JWT token with custom payload
21
+ */
22
+ export declare function generateToken<T extends object>(payload: T, expiresIn?: string): string;
23
+ /**
24
+ * Generate a long-lived token (e.g., for machine/API access)
25
+ */
26
+ export declare function generateLongLivedToken<T extends object>(payload: T, expiresInDays?: number): string;
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.initAuth = initAuth;
7
+ exports.getJwtSecret = getJwtSecret;
8
+ exports.extractToken = extractToken;
9
+ exports.validateToken = validateToken;
10
+ exports.generateToken = generateToken;
11
+ exports.generateLongLivedToken = generateLongLivedToken;
12
+ const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
13
+ /**
14
+ * JWT token utilities - works with any payload structure
15
+ * Use BaseJwtPayload or extend it for type safety
16
+ */
17
+ let jwtSecret = null;
18
+ /**
19
+ * Initialize JWT secret - call once at startup
20
+ * Fails fast if secret is missing or too short
21
+ */
22
+ function initAuth(secret, minLength = 32) {
23
+ if (!secret || secret.length < minLength) {
24
+ throw new Error(`JWT_SECRET must be at least ${minLength} characters`);
25
+ }
26
+ jwtSecret = secret;
27
+ }
28
+ /**
29
+ * Get the JWT secret (throws if not initialized)
30
+ */
31
+ function getJwtSecret() {
32
+ if (!jwtSecret) {
33
+ throw new Error('Auth not initialized. Call initAuth() first.');
34
+ }
35
+ return jwtSecret;
36
+ }
37
+ /**
38
+ * Extract Bearer token from Authorization header
39
+ */
40
+ function extractToken(authHeader) {
41
+ if (!authHeader?.startsWith('Bearer ')) {
42
+ return null;
43
+ }
44
+ return authHeader.slice(7);
45
+ }
46
+ /**
47
+ * Validate and decode a JWT token
48
+ * Generic type allows project-specific payload shapes
49
+ */
50
+ function validateToken(token) {
51
+ try {
52
+ const payload = jsonwebtoken_1.default.verify(token, getJwtSecret());
53
+ if (typeof payload === 'object' && payload !== null) {
54
+ return payload;
55
+ }
56
+ return null;
57
+ }
58
+ catch {
59
+ return null;
60
+ }
61
+ }
62
+ /**
63
+ * Generate a JWT token with custom payload
64
+ */
65
+ function generateToken(payload, expiresIn = '7d') {
66
+ return jsonwebtoken_1.default.sign(payload, getJwtSecret(), { expiresIn });
67
+ }
68
+ /**
69
+ * Generate a long-lived token (e.g., for machine/API access)
70
+ */
71
+ function generateLongLivedToken(payload, expiresInDays = 3650) {
72
+ return jsonwebtoken_1.default.sign(payload, getJwtSecret(), { expiresIn: `${expiresInDays}d` });
73
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Base JWT payload - all tokens include these fields
3
+ * Projects extend this with their specific fields
4
+ */
5
+ export interface BaseJwtPayload {
6
+ iat: number;
7
+ exp: number;
8
+ }
9
+ /**
10
+ * Standard user token payload
11
+ * Used by: azure-pwa-starter, azure-alert-service (admin), onsite-monitor
12
+ */
13
+ export interface UserTokenPayload extends BaseJwtPayload {
14
+ authenticated: true;
15
+ tokenType: 'user' | 'machine';
16
+ }
17
+ /**
18
+ * Username-based token payload
19
+ * Used by: financial-tracker
20
+ */
21
+ export interface UsernameTokenPayload extends BaseJwtPayload {
22
+ username: string;
23
+ }
24
+ /**
25
+ * Role-based token payload
26
+ * Used by: azure-alert-service
27
+ */
28
+ export interface RoleTokenPayload extends BaseJwtPayload {
29
+ authenticated: true;
30
+ tokenType: 'user' | 'machine';
31
+ role: 'admin' | 'viewer';
32
+ viewerTokenId?: string;
33
+ }
34
+ /**
35
+ * Type guard to check if payload has a username
36
+ */
37
+ export declare function hasUsername(payload: BaseJwtPayload): payload is UsernameTokenPayload;
38
+ /**
39
+ * Type guard to check if payload has a role
40
+ */
41
+ export declare function hasRole(payload: BaseJwtPayload): payload is RoleTokenPayload;
42
+ /**
43
+ * Type guard to check if payload is admin
44
+ */
45
+ export declare function isAdmin(payload: BaseJwtPayload): boolean;
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.hasUsername = hasUsername;
4
+ exports.hasRole = hasRole;
5
+ exports.isAdmin = isAdmin;
6
+ /**
7
+ * Type guard to check if payload has a username
8
+ */
9
+ function hasUsername(payload) {
10
+ return 'username' in payload && typeof payload.username === 'string';
11
+ }
12
+ /**
13
+ * Type guard to check if payload has a role
14
+ */
15
+ function hasRole(payload) {
16
+ return 'role' in payload;
17
+ }
18
+ /**
19
+ * Type guard to check if payload is admin
20
+ */
21
+ function isAdmin(payload) {
22
+ return hasRole(payload) && payload.role === 'admin';
23
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Custom error class for API errors
3
+ * Preserves status code and error message from server
4
+ */
5
+ export declare class ApiError extends Error {
6
+ status: number;
7
+ details?: string | undefined;
8
+ constructor(status: number, message: string, details?: string | undefined);
9
+ /**
10
+ * Check if this is an authentication error
11
+ */
12
+ isUnauthorized(): boolean;
13
+ /**
14
+ * Check if this is a not found error
15
+ */
16
+ isNotFound(): boolean;
17
+ /**
18
+ * Check if this is a validation error
19
+ */
20
+ isBadRequest(): boolean;
21
+ }
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ApiError = void 0;
4
+ /**
5
+ * Custom error class for API errors
6
+ * Preserves status code and error message from server
7
+ */
8
+ class ApiError extends Error {
9
+ constructor(status, message, details) {
10
+ super(message);
11
+ this.status = status;
12
+ this.details = details;
13
+ this.name = 'ApiError';
14
+ }
15
+ /**
16
+ * Check if this is an authentication error
17
+ */
18
+ isUnauthorized() {
19
+ return this.status === 401;
20
+ }
21
+ /**
22
+ * Check if this is a not found error
23
+ */
24
+ isNotFound() {
25
+ return this.status === 404;
26
+ }
27
+ /**
28
+ * Check if this is a validation error
29
+ */
30
+ isBadRequest() {
31
+ return this.status === 400;
32
+ }
33
+ }
34
+ exports.ApiError = ApiError;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Initialize the API client
3
+ */
4
+ export declare function initApiClient(config: {
5
+ getToken: () => string | null;
6
+ onUnauthorized?: () => void;
7
+ }): void;
8
+ /**
9
+ * API response wrapper
10
+ */
11
+ export interface ApiResponse<T> {
12
+ ok: boolean;
13
+ status: number;
14
+ data?: T;
15
+ error?: string;
16
+ }
17
+ /**
18
+ * Make an authenticated API call
19
+ */
20
+ export declare function apiCall<T>(url: string, options?: RequestInit): Promise<T>;
21
+ /**
22
+ * GET request helper
23
+ */
24
+ export declare function apiGet<T>(url: string): Promise<T>;
25
+ /**
26
+ * POST request helper
27
+ */
28
+ export declare function apiPost<T>(url: string, body?: unknown): Promise<T>;
29
+ /**
30
+ * PUT request helper
31
+ */
32
+ export declare function apiPut<T>(url: string, body?: unknown): Promise<T>;
33
+ /**
34
+ * PATCH request helper
35
+ */
36
+ export declare function apiPatch<T>(url: string, body?: unknown): Promise<T>;
37
+ /**
38
+ * DELETE request helper
39
+ */
40
+ export declare function apiDelete<T>(url: string): Promise<T>;
41
+ /**
42
+ * Alternative: Response wrapper style (like financial-tracker's authJsonFetch)
43
+ */
44
+ export declare function apiCallSafe<T>(url: string, options?: RequestInit): Promise<ApiResponse<T>>;
@@ -0,0 +1,116 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.initApiClient = initApiClient;
4
+ exports.apiCall = apiCall;
5
+ exports.apiGet = apiGet;
6
+ exports.apiPost = apiPost;
7
+ exports.apiPut = apiPut;
8
+ exports.apiPatch = apiPatch;
9
+ exports.apiDelete = apiDelete;
10
+ exports.apiCallSafe = apiCallSafe;
11
+ const ApiError_1 = require("./ApiError");
12
+ /**
13
+ * Client-side API utilities for authenticated requests
14
+ */
15
+ // Token getter function - set by consuming app
16
+ let getToken = null;
17
+ // Callback for 401 responses (e.g., redirect to login)
18
+ let onUnauthorized = null;
19
+ /**
20
+ * Initialize the API client
21
+ */
22
+ function initApiClient(config) {
23
+ getToken = config.getToken;
24
+ onUnauthorized = config.onUnauthorized ?? null;
25
+ }
26
+ /**
27
+ * Make an authenticated API call
28
+ */
29
+ async function apiCall(url, options = {}) {
30
+ const token = getToken?.();
31
+ const headers = {
32
+ 'Content-Type': 'application/json',
33
+ ...options.headers
34
+ };
35
+ if (token) {
36
+ headers['Authorization'] = `Bearer ${token}`;
37
+ }
38
+ const response = await fetch(url, {
39
+ ...options,
40
+ headers
41
+ });
42
+ if (!response.ok) {
43
+ if (response.status === 401 && onUnauthorized) {
44
+ onUnauthorized();
45
+ }
46
+ let errorMessage = 'Request failed';
47
+ try {
48
+ const errorData = (await response.json());
49
+ errorMessage = errorData.error || errorMessage;
50
+ }
51
+ catch {
52
+ // Ignore JSON parse errors
53
+ }
54
+ throw new ApiError_1.ApiError(response.status, errorMessage);
55
+ }
56
+ // Handle empty responses
57
+ const text = await response.text();
58
+ if (!text) {
59
+ return {};
60
+ }
61
+ return JSON.parse(text);
62
+ }
63
+ /**
64
+ * GET request helper
65
+ */
66
+ async function apiGet(url) {
67
+ return apiCall(url, { method: 'GET' });
68
+ }
69
+ /**
70
+ * POST request helper
71
+ */
72
+ async function apiPost(url, body) {
73
+ return apiCall(url, {
74
+ method: 'POST',
75
+ body: body ? JSON.stringify(body) : undefined
76
+ });
77
+ }
78
+ /**
79
+ * PUT request helper
80
+ */
81
+ async function apiPut(url, body) {
82
+ return apiCall(url, {
83
+ method: 'PUT',
84
+ body: body ? JSON.stringify(body) : undefined
85
+ });
86
+ }
87
+ /**
88
+ * PATCH request helper
89
+ */
90
+ async function apiPatch(url, body) {
91
+ return apiCall(url, {
92
+ method: 'PATCH',
93
+ body: body ? JSON.stringify(body) : undefined
94
+ });
95
+ }
96
+ /**
97
+ * DELETE request helper
98
+ */
99
+ async function apiDelete(url) {
100
+ return apiCall(url, { method: 'DELETE' });
101
+ }
102
+ /**
103
+ * Alternative: Response wrapper style (like financial-tracker's authJsonFetch)
104
+ */
105
+ async function apiCallSafe(url, options = {}) {
106
+ try {
107
+ const data = await apiCall(url, options);
108
+ return { ok: true, status: 200, data };
109
+ }
110
+ catch (error) {
111
+ if (error instanceof ApiError_1.ApiError) {
112
+ return { ok: false, status: error.status, error: error.message };
113
+ }
114
+ return { ok: false, status: 0, error: 'Network error' };
115
+ }
116
+ }
@@ -0,0 +1,2 @@
1
+ export { ApiError } from './ApiError';
2
+ export { initApiClient, type ApiResponse, apiCall, apiGet, apiPost, apiPut, apiPatch, apiDelete, apiCallSafe } from './api';
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.apiCallSafe = exports.apiDelete = exports.apiPatch = exports.apiPut = exports.apiPost = exports.apiGet = exports.apiCall = exports.initApiClient = exports.ApiError = void 0;
4
+ var ApiError_1 = require("./ApiError");
5
+ Object.defineProperty(exports, "ApiError", { enumerable: true, get: function () { return ApiError_1.ApiError; } });
6
+ var api_1 = require("./api");
7
+ Object.defineProperty(exports, "initApiClient", { enumerable: true, get: function () { return api_1.initApiClient; } });
8
+ Object.defineProperty(exports, "apiCall", { enumerable: true, get: function () { return api_1.apiCall; } });
9
+ Object.defineProperty(exports, "apiGet", { enumerable: true, get: function () { return api_1.apiGet; } });
10
+ Object.defineProperty(exports, "apiPost", { enumerable: true, get: function () { return api_1.apiPost; } });
11
+ Object.defineProperty(exports, "apiPut", { enumerable: true, get: function () { return api_1.apiPut; } });
12
+ Object.defineProperty(exports, "apiPatch", { enumerable: true, get: function () { return api_1.apiPatch; } });
13
+ Object.defineProperty(exports, "apiDelete", { enumerable: true, get: function () { return api_1.apiDelete; } });
14
+ Object.defineProperty(exports, "apiCallSafe", { enumerable: true, get: function () { return api_1.apiCallSafe; } });
@@ -0,0 +1,2 @@
1
+ export { HTTP_STATUS, type HttpStatus } from './status';
2
+ export { type ErrorResponse, badRequestResponse, unauthorizedResponse, forbiddenResponse, notFoundResponse, conflictResponse, handleFunctionError, isNotFoundError, isConflictError } from './responses';
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isConflictError = exports.isNotFoundError = exports.handleFunctionError = exports.conflictResponse = exports.notFoundResponse = exports.forbiddenResponse = exports.unauthorizedResponse = exports.badRequestResponse = exports.HTTP_STATUS = void 0;
4
+ var status_1 = require("./status");
5
+ Object.defineProperty(exports, "HTTP_STATUS", { enumerable: true, get: function () { return status_1.HTTP_STATUS; } });
6
+ var responses_1 = require("./responses");
7
+ Object.defineProperty(exports, "badRequestResponse", { enumerable: true, get: function () { return responses_1.badRequestResponse; } });
8
+ Object.defineProperty(exports, "unauthorizedResponse", { enumerable: true, get: function () { return responses_1.unauthorizedResponse; } });
9
+ Object.defineProperty(exports, "forbiddenResponse", { enumerable: true, get: function () { return responses_1.forbiddenResponse; } });
10
+ Object.defineProperty(exports, "notFoundResponse", { enumerable: true, get: function () { return responses_1.notFoundResponse; } });
11
+ Object.defineProperty(exports, "conflictResponse", { enumerable: true, get: function () { return responses_1.conflictResponse; } });
12
+ Object.defineProperty(exports, "handleFunctionError", { enumerable: true, get: function () { return responses_1.handleFunctionError; } });
13
+ Object.defineProperty(exports, "isNotFoundError", { enumerable: true, get: function () { return responses_1.isNotFoundError; } });
14
+ Object.defineProperty(exports, "isConflictError", { enumerable: true, get: function () { return responses_1.isConflictError; } });
@@ -0,0 +1,40 @@
1
+ import { HttpResponseInit, InvocationContext } from '@azure/functions';
2
+ /**
3
+ * Standard error response structure
4
+ */
5
+ export interface ErrorResponse {
6
+ error: string;
7
+ details?: string;
8
+ }
9
+ /**
10
+ * Create a 400 Bad Request response
11
+ */
12
+ export declare function badRequestResponse(message: string): HttpResponseInit;
13
+ /**
14
+ * Create a 401 Unauthorized response
15
+ */
16
+ export declare function unauthorizedResponse(message?: string): HttpResponseInit;
17
+ /**
18
+ * Create a 403 Forbidden response
19
+ */
20
+ export declare function forbiddenResponse(message?: string): HttpResponseInit;
21
+ /**
22
+ * Create a 404 Not Found response
23
+ */
24
+ export declare function notFoundResponse(resource: string): HttpResponseInit;
25
+ /**
26
+ * Create a 409 Conflict response
27
+ */
28
+ export declare function conflictResponse(message: string): HttpResponseInit;
29
+ /**
30
+ * Handle unexpected errors safely (logs details, returns generic message)
31
+ */
32
+ export declare function handleFunctionError(error: unknown, context: InvocationContext): HttpResponseInit;
33
+ /**
34
+ * Check if an Azure Table Storage error is "not found"
35
+ */
36
+ export declare function isNotFoundError(error: unknown): boolean;
37
+ /**
38
+ * Check if an Azure Table Storage error is "conflict"
39
+ */
40
+ export declare function isConflictError(error: unknown): boolean;
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.badRequestResponse = badRequestResponse;
4
+ exports.unauthorizedResponse = unauthorizedResponse;
5
+ exports.forbiddenResponse = forbiddenResponse;
6
+ exports.notFoundResponse = notFoundResponse;
7
+ exports.conflictResponse = conflictResponse;
8
+ exports.handleFunctionError = handleFunctionError;
9
+ exports.isNotFoundError = isNotFoundError;
10
+ exports.isConflictError = isConflictError;
11
+ const status_1 = require("./status");
12
+ /**
13
+ * Create a 400 Bad Request response
14
+ */
15
+ function badRequestResponse(message) {
16
+ return {
17
+ status: status_1.HTTP_STATUS.BAD_REQUEST,
18
+ jsonBody: { error: message }
19
+ };
20
+ }
21
+ /**
22
+ * Create a 401 Unauthorized response
23
+ */
24
+ function unauthorizedResponse(message = 'Unauthorized') {
25
+ return {
26
+ status: status_1.HTTP_STATUS.UNAUTHORIZED,
27
+ jsonBody: { error: message }
28
+ };
29
+ }
30
+ /**
31
+ * Create a 403 Forbidden response
32
+ */
33
+ function forbiddenResponse(message = 'Forbidden') {
34
+ return {
35
+ status: status_1.HTTP_STATUS.FORBIDDEN,
36
+ jsonBody: { error: message }
37
+ };
38
+ }
39
+ /**
40
+ * Create a 404 Not Found response
41
+ */
42
+ function notFoundResponse(resource) {
43
+ return {
44
+ status: status_1.HTTP_STATUS.NOT_FOUND,
45
+ jsonBody: { error: `${resource} not found` }
46
+ };
47
+ }
48
+ /**
49
+ * Create a 409 Conflict response
50
+ */
51
+ function conflictResponse(message) {
52
+ return {
53
+ status: status_1.HTTP_STATUS.CONFLICT,
54
+ jsonBody: { error: message }
55
+ };
56
+ }
57
+ /**
58
+ * Handle unexpected errors safely (logs details, returns generic message)
59
+ */
60
+ function handleFunctionError(error, context) {
61
+ const message = error instanceof Error ? error.message : 'Unknown error';
62
+ context.error(`Function error: ${message}`);
63
+ return {
64
+ status: status_1.HTTP_STATUS.INTERNAL_ERROR,
65
+ jsonBody: { error: 'Internal server error' }
66
+ };
67
+ }
68
+ /**
69
+ * Check if an Azure Table Storage error is "not found"
70
+ */
71
+ function isNotFoundError(error) {
72
+ return (error instanceof Error &&
73
+ 'statusCode' in error &&
74
+ error.statusCode === 404);
75
+ }
76
+ /**
77
+ * Check if an Azure Table Storage error is "conflict"
78
+ */
79
+ function isConflictError(error) {
80
+ return (error instanceof Error &&
81
+ 'statusCode' in error &&
82
+ error.statusCode === 409);
83
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * HTTP status codes - use instead of magic numbers
3
+ */
4
+ export declare const HTTP_STATUS: {
5
+ readonly OK: 200;
6
+ readonly CREATED: 201;
7
+ readonly NO_CONTENT: 204;
8
+ readonly BAD_REQUEST: 400;
9
+ readonly UNAUTHORIZED: 401;
10
+ readonly FORBIDDEN: 403;
11
+ readonly NOT_FOUND: 404;
12
+ readonly CONFLICT: 409;
13
+ readonly GONE: 410;
14
+ readonly INTERNAL_ERROR: 500;
15
+ readonly SERVICE_UNAVAILABLE: 503;
16
+ };
17
+ export type HttpStatus = typeof HTTP_STATUS[keyof typeof HTTP_STATUS];
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HTTP_STATUS = void 0;
4
+ /**
5
+ * HTTP status codes - use instead of magic numbers
6
+ */
7
+ exports.HTTP_STATUS = {
8
+ OK: 200,
9
+ CREATED: 201,
10
+ NO_CONTENT: 204,
11
+ BAD_REQUEST: 400,
12
+ UNAUTHORIZED: 401,
13
+ FORBIDDEN: 403,
14
+ NOT_FOUND: 404,
15
+ CONFLICT: 409,
16
+ GONE: 410,
17
+ INTERNAL_ERROR: 500,
18
+ SERVICE_UNAVAILABLE: 503
19
+ };
@@ -0,0 +1,4 @@
1
+ export * from './auth';
2
+ export * from './http';
3
+ export * from './storage';
4
+ export * from './client';
package/dist/index.js ADDED
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ // Main entry point - re-export everything
18
+ __exportStar(require("./auth"), exports);
19
+ __exportStar(require("./http"), exports);
20
+ __exportStar(require("./storage"), exports);
21
+ __exportStar(require("./client"), exports);
@@ -0,0 +1,25 @@
1
+ import { TableClient } from '@azure/data-tables';
2
+ /**
3
+ * Initialize storage configuration - call once at startup
4
+ */
5
+ export declare function initStorage(config: {
6
+ accountName?: string;
7
+ connectionString?: string;
8
+ }): void;
9
+ /**
10
+ * Auto-initialize from environment variables
11
+ */
12
+ export declare function initStorageFromEnv(): void;
13
+ /**
14
+ * Check if using managed identity (Azure) vs connection string (local)
15
+ */
16
+ export declare function useManagedIdentity(): boolean;
17
+ /**
18
+ * Get a TableClient for the specified table
19
+ * Creates table if it doesn't exist, caches client for reuse
20
+ */
21
+ export declare function getTableClient(tableName: string): Promise<TableClient>;
22
+ /**
23
+ * Clear the client cache (useful for testing)
24
+ */
25
+ export declare function clearTableClientCache(): void;
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.initStorage = initStorage;
4
+ exports.initStorageFromEnv = initStorageFromEnv;
5
+ exports.useManagedIdentity = useManagedIdentity;
6
+ exports.getTableClient = getTableClient;
7
+ exports.clearTableClientCache = clearTableClientCache;
8
+ const data_tables_1 = require("@azure/data-tables");
9
+ const identity_1 = require("@azure/identity");
10
+ /**
11
+ * Azure Table Storage client factory with managed identity support
12
+ */
13
+ // Cache table clients to avoid repeated connections
14
+ const tableClients = new Map();
15
+ // Configuration
16
+ let storageAccountName;
17
+ let connectionString;
18
+ /**
19
+ * Initialize storage configuration - call once at startup
20
+ */
21
+ function initStorage(config) {
22
+ storageAccountName = config.accountName;
23
+ connectionString = config.connectionString;
24
+ }
25
+ /**
26
+ * Auto-initialize from environment variables
27
+ */
28
+ function initStorageFromEnv() {
29
+ initStorage({
30
+ accountName: process.env.STORAGE_ACCOUNT_NAME,
31
+ connectionString: process.env.AzureWebJobsStorage
32
+ });
33
+ }
34
+ /**
35
+ * Check if using managed identity (Azure) vs connection string (local)
36
+ */
37
+ function useManagedIdentity() {
38
+ return !!storageAccountName && !connectionString?.includes('UseDevelopmentStorage');
39
+ }
40
+ /**
41
+ * Get a TableClient for the specified table
42
+ * Creates table if it doesn't exist, caches client for reuse
43
+ */
44
+ async function getTableClient(tableName) {
45
+ // Return cached client if available
46
+ const cached = tableClients.get(tableName);
47
+ if (cached) {
48
+ return cached;
49
+ }
50
+ let client;
51
+ if (useManagedIdentity()) {
52
+ // Azure: Use managed identity
53
+ const credential = new identity_1.DefaultAzureCredential();
54
+ const endpoint = `https://${storageAccountName}.table.core.windows.net`;
55
+ client = new data_tables_1.TableClient(endpoint, tableName, credential);
56
+ }
57
+ else if (connectionString) {
58
+ // Local/Azurite: Use connection string
59
+ client = data_tables_1.TableClient.fromConnectionString(connectionString, tableName);
60
+ }
61
+ else {
62
+ throw new Error('Storage not configured. Set STORAGE_ACCOUNT_NAME or AzureWebJobsStorage.');
63
+ }
64
+ // Create table if it doesn't exist
65
+ try {
66
+ await client.createTable();
67
+ }
68
+ catch (error) {
69
+ // Ignore "table already exists" errors (409 Conflict)
70
+ const tableError = error;
71
+ if (tableError.statusCode !== 409) {
72
+ throw error;
73
+ }
74
+ }
75
+ // Cache and return
76
+ tableClients.set(tableName, client);
77
+ return client;
78
+ }
79
+ /**
80
+ * Clear the client cache (useful for testing)
81
+ */
82
+ function clearTableClientCache() {
83
+ tableClients.clear();
84
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Generate a unique row key from an identifier string
3
+ * Uses SHA-256 hash for consistent, URL-safe keys
4
+ *
5
+ * @param identifier - The string to hash (e.g., subscription endpoint, user ID)
6
+ * @returns A 32-character hex string suitable for Azure Table Storage row keys
7
+ */
8
+ export declare function generateRowKey(identifier: string): string;
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateRowKey = generateRowKey;
4
+ const crypto_1 = require("crypto");
5
+ /**
6
+ * Generate a unique row key from an identifier string
7
+ * Uses SHA-256 hash for consistent, URL-safe keys
8
+ *
9
+ * @param identifier - The string to hash (e.g., subscription endpoint, user ID)
10
+ * @returns A 32-character hex string suitable for Azure Table Storage row keys
11
+ */
12
+ function generateRowKey(identifier) {
13
+ return (0, crypto_1.createHash)('sha256').update(identifier).digest('hex').substring(0, 32);
14
+ }
@@ -0,0 +1,2 @@
1
+ export { initStorage, initStorageFromEnv, useManagedIdentity, getTableClient, clearTableClientCache } from './client';
2
+ export { generateRowKey } from './helpers';
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateRowKey = exports.clearTableClientCache = exports.getTableClient = exports.useManagedIdentity = exports.initStorageFromEnv = exports.initStorage = void 0;
4
+ var client_1 = require("./client");
5
+ Object.defineProperty(exports, "initStorage", { enumerable: true, get: function () { return client_1.initStorage; } });
6
+ Object.defineProperty(exports, "initStorageFromEnv", { enumerable: true, get: function () { return client_1.initStorageFromEnv; } });
7
+ Object.defineProperty(exports, "useManagedIdentity", { enumerable: true, get: function () { return client_1.useManagedIdentity; } });
8
+ Object.defineProperty(exports, "getTableClient", { enumerable: true, get: function () { return client_1.getTableClient; } });
9
+ Object.defineProperty(exports, "clearTableClientCache", { enumerable: true, get: function () { return client_1.clearTableClientCache; } });
10
+ var helpers_1 = require("./helpers");
11
+ Object.defineProperty(exports, "generateRowKey", { enumerable: true, get: function () { return helpers_1.generateRowKey; } });
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@markwharton/pwa-core",
3
+ "version": "1.0.0",
4
+ "description": "Shared patterns for Azure PWA projects",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": "./dist/index.js",
9
+ "./auth": "./dist/auth/index.js",
10
+ "./http": "./dist/http/index.js",
11
+ "./storage": "./dist/storage/index.js",
12
+ "./client": "./dist/client/index.js"
13
+ },
14
+ "scripts": {
15
+ "build": "tsc",
16
+ "clean": "rm -rf dist"
17
+ },
18
+ "peerDependencies": {
19
+ "@azure/data-tables": "^13.0.0",
20
+ "@azure/identity": "^4.0.0",
21
+ "jsonwebtoken": "^9.0.0"
22
+ },
23
+ "devDependencies": {
24
+ "@azure/data-tables": "^13.2.2",
25
+ "@azure/functions": "^4.5.0",
26
+ "@azure/identity": "^4.5.0",
27
+ "@types/jsonwebtoken": "^9.0.5",
28
+ "@types/node": "^20.10.0",
29
+ "jsonwebtoken": "^9.0.2",
30
+ "typescript": "^5.3.0"
31
+ },
32
+ "files": [
33
+ "dist"
34
+ ],
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "https://github.com/MarkWharton/pwa-packages.git",
38
+ "directory": "packages/core"
39
+ },
40
+ "author": "Mark Wharton",
41
+ "license": "MIT"
42
+ }