@markwharton/pwa-core 1.7.0 → 2.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.
Files changed (103) hide show
  1. package/dist/{client/api.d.ts → client.d.ts} +85 -9
  2. package/dist/{client/api.js → client.js} +159 -56
  3. package/dist/index.d.ts +10 -2
  4. package/dist/index.js +14 -6
  5. package/dist/server.d.ts +283 -0
  6. package/dist/server.js +476 -0
  7. package/dist/shared.d.ts +150 -0
  8. package/dist/shared.js +124 -0
  9. package/package.json +11 -12
  10. package/dist/__tests__/auth/apiKey.test.d.ts +0 -1
  11. package/dist/__tests__/auth/apiKey.test.js +0 -80
  12. package/dist/__tests__/auth/token.test.d.ts +0 -1
  13. package/dist/__tests__/auth/token.test.js +0 -212
  14. package/dist/__tests__/auth/types.test.d.ts +0 -1
  15. package/dist/__tests__/auth/types.test.js +0 -77
  16. package/dist/__tests__/client/api.test.d.ts +0 -1
  17. package/dist/__tests__/client/api.test.js +0 -369
  18. package/dist/__tests__/client/apiError.test.d.ts +0 -1
  19. package/dist/__tests__/client/apiError.test.js +0 -91
  20. package/dist/__tests__/http/responses.test.d.ts +0 -1
  21. package/dist/__tests__/http/responses.test.js +0 -112
  22. package/dist/__tests__/http/status.test.d.ts +0 -1
  23. package/dist/__tests__/http/status.test.js +0 -27
  24. package/dist/__tests__/server/auth/apiKey.test.d.ts +0 -1
  25. package/dist/__tests__/server/auth/apiKey.test.js +0 -80
  26. package/dist/__tests__/server/auth/token.test.d.ts +0 -1
  27. package/dist/__tests__/server/auth/token.test.js +0 -299
  28. package/dist/__tests__/server/http/responses.test.d.ts +0 -1
  29. package/dist/__tests__/server/http/responses.test.js +0 -112
  30. package/dist/__tests__/server/storage/client.test.d.ts +0 -1
  31. package/dist/__tests__/server/storage/client.test.js +0 -173
  32. package/dist/__tests__/server/storage/keys.test.d.ts +0 -1
  33. package/dist/__tests__/server/storage/keys.test.js +0 -47
  34. package/dist/__tests__/shared/auth/types.test.d.ts +0 -1
  35. package/dist/__tests__/shared/auth/types.test.js +0 -77
  36. package/dist/__tests__/shared/http/status.test.d.ts +0 -1
  37. package/dist/__tests__/shared/http/status.test.js +0 -29
  38. package/dist/__tests__/storage/client.test.d.ts +0 -1
  39. package/dist/__tests__/storage/client.test.js +0 -173
  40. package/dist/__tests__/storage/keys.test.d.ts +0 -1
  41. package/dist/__tests__/storage/keys.test.js +0 -47
  42. package/dist/__tests__/types.test.d.ts +0 -1
  43. package/dist/__tests__/types.test.js +0 -56
  44. package/dist/auth/apiKey.d.ts +0 -44
  45. package/dist/auth/apiKey.js +0 -59
  46. package/dist/auth/index.d.ts +0 -3
  47. package/dist/auth/index.js +0 -22
  48. package/dist/auth/token.d.ts +0 -56
  49. package/dist/auth/token.js +0 -104
  50. package/dist/auth/types.d.ts +0 -63
  51. package/dist/auth/types.js +0 -41
  52. package/dist/client/apiError.d.ts +0 -48
  53. package/dist/client/apiError.js +0 -65
  54. package/dist/client/index.d.ts +0 -3
  55. package/dist/client/index.js +0 -14
  56. package/dist/client/types.d.ts +0 -12
  57. package/dist/client/types.js +0 -5
  58. package/dist/http/index.d.ts +0 -3
  59. package/dist/http/index.js +0 -14
  60. package/dist/http/responses.d.ts +0 -82
  61. package/dist/http/responses.js +0 -132
  62. package/dist/http/status.d.ts +0 -17
  63. package/dist/http/status.js +0 -19
  64. package/dist/http/types.d.ts +0 -10
  65. package/dist/http/types.js +0 -5
  66. package/dist/server/auth/apiKey.d.ts +0 -44
  67. package/dist/server/auth/apiKey.js +0 -59
  68. package/dist/server/auth/index.d.ts +0 -3
  69. package/dist/server/auth/index.js +0 -19
  70. package/dist/server/auth/token.d.ts +0 -102
  71. package/dist/server/auth/token.js +0 -158
  72. package/dist/server/http/index.d.ts +0 -1
  73. package/dist/server/http/index.js +0 -12
  74. package/dist/server/http/responses.d.ts +0 -82
  75. package/dist/server/http/responses.js +0 -132
  76. package/dist/server/index.d.ts +0 -4
  77. package/dist/server/index.js +0 -37
  78. package/dist/server/storage/client.d.ts +0 -48
  79. package/dist/server/storage/client.js +0 -107
  80. package/dist/server/storage/index.d.ts +0 -2
  81. package/dist/server/storage/index.js +0 -11
  82. package/dist/server/storage/keys.d.ts +0 -8
  83. package/dist/server/storage/keys.js +0 -14
  84. package/dist/shared/auth/index.d.ts +0 -2
  85. package/dist/shared/auth/index.js +0 -7
  86. package/dist/shared/auth/types.d.ts +0 -63
  87. package/dist/shared/auth/types.js +0 -41
  88. package/dist/shared/http/index.d.ts +0 -3
  89. package/dist/shared/http/index.js +0 -5
  90. package/dist/shared/http/status.d.ts +0 -19
  91. package/dist/shared/http/status.js +0 -21
  92. package/dist/shared/http/types.d.ts +0 -10
  93. package/dist/shared/http/types.js +0 -5
  94. package/dist/shared/index.d.ts +0 -5
  95. package/dist/shared/index.js +0 -10
  96. package/dist/storage/client.d.ts +0 -48
  97. package/dist/storage/client.js +0 -107
  98. package/dist/storage/index.d.ts +0 -2
  99. package/dist/storage/index.js +0 -11
  100. package/dist/storage/keys.d.ts +0 -8
  101. package/dist/storage/keys.js +0 -14
  102. package/dist/types.d.ts +0 -48
  103. package/dist/types.js +0 -41
package/dist/server.js ADDED
@@ -0,0 +1,476 @@
1
+ "use strict";
2
+ /**
3
+ * pwa-core/server - Server-side utilities for Azure Functions
4
+ *
5
+ * Includes: JWT auth, API keys, HTTP responses, Azure Table Storage
6
+ */
7
+ var __importDefault = (this && this.__importDefault) || function (mod) {
8
+ return (mod && mod.__esModule) ? mod : { "default": mod };
9
+ };
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.HTTP_STATUS = exports.isAdmin = exports.hasRole = exports.hasUsername = exports.getErrorMessage = exports.err = exports.okVoid = exports.ok = exports.DEFAULT_TOKEN_EXPIRY_SECONDS = exports.DEFAULT_TOKEN_EXPIRY = void 0;
12
+ exports.initAuth = initAuth;
13
+ exports.getJwtSecret = getJwtSecret;
14
+ exports.extractToken = extractToken;
15
+ exports.validateToken = validateToken;
16
+ exports.generateToken = generateToken;
17
+ exports.generateLongLivedToken = generateLongLivedToken;
18
+ exports.requireAuth = requireAuth;
19
+ exports.requireAdmin = requireAdmin;
20
+ exports.extractApiKey = extractApiKey;
21
+ exports.hashApiKey = hashApiKey;
22
+ exports.validateApiKey = validateApiKey;
23
+ exports.generateApiKey = generateApiKey;
24
+ exports.badRequestResponse = badRequestResponse;
25
+ exports.unauthorizedResponse = unauthorizedResponse;
26
+ exports.forbiddenResponse = forbiddenResponse;
27
+ exports.notFoundResponse = notFoundResponse;
28
+ exports.conflictResponse = conflictResponse;
29
+ exports.handleFunctionError = handleFunctionError;
30
+ exports.isNotFoundError = isNotFoundError;
31
+ exports.isConflictError = isConflictError;
32
+ exports.initStorage = initStorage;
33
+ exports.initStorageFromEnv = initStorageFromEnv;
34
+ exports.useManagedIdentity = useManagedIdentity;
35
+ exports.getTableClient = getTableClient;
36
+ exports.clearTableClientCache = clearTableClientCache;
37
+ exports.generateRowKey = generateRowKey;
38
+ const crypto_1 = require("crypto");
39
+ const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
40
+ const data_tables_1 = require("@azure/data-tables");
41
+ const identity_1 = require("@azure/identity");
42
+ const shared_1 = require("./shared");
43
+ // =============================================================================
44
+ // JWT Authentication
45
+ // =============================================================================
46
+ let jwtSecret = null;
47
+ /**
48
+ * Initializes the JWT authentication system. Call once at application startup.
49
+ * @param secret - The JWT secret key (from environment variable)
50
+ * @param minLength - Minimum required secret length (default: 32)
51
+ * @throws Error if secret is missing or too short
52
+ * @example
53
+ * initAuth(process.env.JWT_SECRET);
54
+ */
55
+ function initAuth(secret, minLength = 32) {
56
+ if (!secret || secret.length < minLength) {
57
+ throw new Error(`JWT_SECRET must be at least ${minLength} characters`);
58
+ }
59
+ jwtSecret = secret;
60
+ }
61
+ /**
62
+ * Gets the configured JWT secret.
63
+ * @returns The JWT secret string
64
+ * @throws Error if initAuth() has not been called
65
+ */
66
+ function getJwtSecret() {
67
+ if (!jwtSecret) {
68
+ throw new Error('Auth not initialized. Call initAuth() first.');
69
+ }
70
+ return jwtSecret;
71
+ }
72
+ /**
73
+ * Extracts the Bearer token from an Authorization header.
74
+ * @param authHeader - The Authorization header value
75
+ * @returns The token string, or null if not a valid Bearer token
76
+ * @example
77
+ * const token = extractToken(request.headers.get('Authorization'));
78
+ */
79
+ function extractToken(authHeader) {
80
+ if (!authHeader?.startsWith('Bearer ')) {
81
+ return null;
82
+ }
83
+ return authHeader.slice(7);
84
+ }
85
+ /**
86
+ * Validates and decodes a JWT token.
87
+ * @typeParam T - The expected payload type (extends object)
88
+ * @param token - The JWT token string to validate
89
+ * @returns Result with decoded payload on success, or error message on failure
90
+ * @example
91
+ * const result = validateToken<UserPayload>(token);
92
+ * if (result.ok) {
93
+ * console.log(result.data.username);
94
+ * }
95
+ */
96
+ function validateToken(token) {
97
+ try {
98
+ const payload = jsonwebtoken_1.default.verify(token, getJwtSecret());
99
+ if (typeof payload === 'object' && payload !== null) {
100
+ return (0, shared_1.ok)(payload);
101
+ }
102
+ return (0, shared_1.err)('Invalid token payload');
103
+ }
104
+ catch (error) {
105
+ return (0, shared_1.err)((0, shared_1.getErrorMessage)(error, 'Token validation failed'));
106
+ }
107
+ }
108
+ /**
109
+ * Generates a signed JWT token with the given payload.
110
+ * @typeParam T - The payload type (extends object)
111
+ * @param payload - The data to encode in the token
112
+ * @param expiresIn - Token expiration time (default: '7d')
113
+ * @returns The signed JWT token string
114
+ * @example
115
+ * const token = generateToken({ userId: '123', role: 'admin' }, '1h');
116
+ */
117
+ function generateToken(payload, expiresIn = '7d') {
118
+ return jsonwebtoken_1.default.sign(payload, getJwtSecret(), { expiresIn });
119
+ }
120
+ /**
121
+ * Generates a long-lived JWT token for machine/API access.
122
+ * @typeParam T - The payload type (extends object)
123
+ * @param payload - The data to encode in the token
124
+ * @param expiresInDays - Token expiration in days (default: 3650 = 10 years)
125
+ * @returns The signed JWT token string
126
+ * @example
127
+ * const apiToken = generateLongLivedToken({ machineId: 'server-1' });
128
+ */
129
+ function generateLongLivedToken(payload, expiresInDays = 3650) {
130
+ return jsonwebtoken_1.default.sign(payload, getJwtSecret(), { expiresIn: `${expiresInDays}d` });
131
+ }
132
+ // =============================================================================
133
+ // Token Expiry Constants
134
+ // =============================================================================
135
+ /** Default token expiry in days (single source of truth) */
136
+ const DEFAULT_TOKEN_EXPIRY_DAYS = 7;
137
+ /** Default token expiry string for generateToken (7 days) */
138
+ exports.DEFAULT_TOKEN_EXPIRY = `${DEFAULT_TOKEN_EXPIRY_DAYS}d`;
139
+ /** Default token expiry in seconds (7 days = 604800 seconds) */
140
+ exports.DEFAULT_TOKEN_EXPIRY_SECONDS = DEFAULT_TOKEN_EXPIRY_DAYS * 24 * 60 * 60;
141
+ // =============================================================================
142
+ // Auth Helpers
143
+ // =============================================================================
144
+ /**
145
+ * Validates auth header and returns typed payload or error response.
146
+ * @typeParam T - The expected payload type (extends BaseJwtPayload)
147
+ * @param authHeader - The Authorization header value
148
+ * @returns AuthResult with payload on success, or HTTP response on failure
149
+ * @example
150
+ * const auth = requireAuth<UsernameTokenPayload>(request.headers.get('Authorization'));
151
+ * if (!auth.authorized) return auth.response;
152
+ * console.log(auth.payload.username);
153
+ */
154
+ function requireAuth(authHeader) {
155
+ const token = extractToken(authHeader);
156
+ if (!token) {
157
+ return { authorized: false, response: unauthorizedResponse() };
158
+ }
159
+ const result = validateToken(token);
160
+ if (!result.ok) {
161
+ return { authorized: false, response: unauthorizedResponse(result.error) };
162
+ }
163
+ return { authorized: true, payload: result.data };
164
+ }
165
+ /**
166
+ * Requires admin role. Use with RoleTokenPayload.
167
+ * @param authHeader - The Authorization header value
168
+ * @returns AuthResult with RoleTokenPayload on success, or HTTP response on failure
169
+ * @example
170
+ * const auth = requireAdmin(request.headers.get('Authorization'));
171
+ * if (!auth.authorized) return auth.response;
172
+ * // User is admin
173
+ */
174
+ function requireAdmin(authHeader) {
175
+ const auth = requireAuth(authHeader);
176
+ if (!auth.authorized)
177
+ return auth;
178
+ if (!(0, shared_1.isAdmin)(auth.payload)) {
179
+ return { authorized: false, response: forbiddenResponse('Admin access required') };
180
+ }
181
+ return auth;
182
+ }
183
+ // =============================================================================
184
+ // API Key Authentication
185
+ // =============================================================================
186
+ /**
187
+ * Extracts API key from the X-API-Key header.
188
+ * @param request - Request object with headers.get() method
189
+ * @returns The API key string, or null if not present
190
+ * @example
191
+ * const apiKey = extractApiKey(request);
192
+ */
193
+ function extractApiKey(request) {
194
+ return request.headers.get('X-API-Key');
195
+ }
196
+ /**
197
+ * Hashes an API key using SHA-256 for secure storage.
198
+ * Store this hash in your database, never the raw key.
199
+ * @param apiKey - The raw API key to hash
200
+ * @returns The SHA-256 hash as a hex string
201
+ * @example
202
+ * const hash = hashApiKey(rawKey);
203
+ * await db.save({ apiKeyHash: hash });
204
+ */
205
+ function hashApiKey(apiKey) {
206
+ return (0, crypto_1.createHash)('sha256').update(apiKey).digest('hex');
207
+ }
208
+ /**
209
+ * Validates an API key against a stored hash.
210
+ * @param apiKey - The API key from the request
211
+ * @param storedHash - The hash stored in your database
212
+ * @returns Result with ok=true if valid, or error message if invalid
213
+ * @example
214
+ * const result = validateApiKey(apiKey, user.apiKeyHash);
215
+ * if (!result.ok) return httpUnauthorized();
216
+ */
217
+ function validateApiKey(apiKey, storedHash) {
218
+ const keyHash = hashApiKey(apiKey);
219
+ if (keyHash === storedHash) {
220
+ return (0, shared_1.okVoid)();
221
+ }
222
+ return (0, shared_1.err)('Invalid API key');
223
+ }
224
+ /**
225
+ * Generates a cryptographically secure API key.
226
+ * @returns A random 64-character hex string (32 bytes)
227
+ * @example
228
+ * const apiKey = generateApiKey();
229
+ * // Return to user once, store hash in database
230
+ */
231
+ function generateApiKey() {
232
+ return (0, crypto_1.randomBytes)(32).toString('hex');
233
+ }
234
+ // =============================================================================
235
+ // HTTP Response Helpers
236
+ // =============================================================================
237
+ /**
238
+ * Creates a 400 Bad Request response.
239
+ * @param message - The error message to return
240
+ * @returns Azure Functions HttpResponseInit object
241
+ * @example
242
+ * if (!body.email) return badRequestResponse('Email is required');
243
+ */
244
+ function badRequestResponse(message) {
245
+ return {
246
+ status: shared_1.HTTP_STATUS.BAD_REQUEST,
247
+ jsonBody: { error: message }
248
+ };
249
+ }
250
+ /**
251
+ * Creates a 401 Unauthorized response.
252
+ * @param message - The error message (default: 'Unauthorized')
253
+ * @returns Azure Functions HttpResponseInit object
254
+ * @example
255
+ * if (!token) return unauthorizedResponse();
256
+ */
257
+ function unauthorizedResponse(message = 'Unauthorized') {
258
+ return {
259
+ status: shared_1.HTTP_STATUS.UNAUTHORIZED,
260
+ jsonBody: { error: message }
261
+ };
262
+ }
263
+ /**
264
+ * Creates a 403 Forbidden response.
265
+ * @param message - The error message (default: 'Forbidden')
266
+ * @returns Azure Functions HttpResponseInit object
267
+ * @example
268
+ * if (!isAdmin(payload)) return forbiddenResponse('Admin access required');
269
+ */
270
+ function forbiddenResponse(message = 'Forbidden') {
271
+ return {
272
+ status: shared_1.HTTP_STATUS.FORBIDDEN,
273
+ jsonBody: { error: message }
274
+ };
275
+ }
276
+ /**
277
+ * Creates a 404 Not Found response.
278
+ * @param resource - The name of the resource that wasn't found
279
+ * @returns Azure Functions HttpResponseInit object
280
+ * @example
281
+ * if (!user) return notFoundResponse('User');
282
+ * // Returns: { error: 'User not found' }
283
+ */
284
+ function notFoundResponse(resource) {
285
+ return {
286
+ status: shared_1.HTTP_STATUS.NOT_FOUND,
287
+ jsonBody: { error: `${resource} not found` }
288
+ };
289
+ }
290
+ /**
291
+ * Creates a 409 Conflict response.
292
+ * @param message - The conflict error message
293
+ * @returns Azure Functions HttpResponseInit object
294
+ * @example
295
+ * if (existingUser) return conflictResponse('Email already registered');
296
+ */
297
+ function conflictResponse(message) {
298
+ return {
299
+ status: shared_1.HTTP_STATUS.CONFLICT,
300
+ jsonBody: { error: message }
301
+ };
302
+ }
303
+ /**
304
+ * Handles unexpected errors safely by logging details and returning a generic message.
305
+ * Use in catch blocks to avoid exposing internal error details to clients.
306
+ * @param error - The caught error
307
+ * @param context - Azure Functions InvocationContext for logging
308
+ * @returns Azure Functions HttpResponseInit with 500 status
309
+ * @example
310
+ * try {
311
+ * await riskyOperation();
312
+ * } catch (error) {
313
+ * return handleFunctionError(error, context);
314
+ * }
315
+ */
316
+ function handleFunctionError(error, context) {
317
+ const message = error instanceof Error ? error.message : 'Unknown error';
318
+ context.error(`Function error: ${message}`);
319
+ return {
320
+ status: shared_1.HTTP_STATUS.INTERNAL_ERROR,
321
+ jsonBody: { error: 'Internal server error' }
322
+ };
323
+ }
324
+ /**
325
+ * Checks if an error is an Azure Table Storage "not found" error.
326
+ * @param error - The caught error
327
+ * @returns True if error has statusCode 404
328
+ * @example
329
+ * try {
330
+ * await tableClient.getEntity(pk, rk);
331
+ * } catch (error) {
332
+ * if (isNotFoundError(error)) return notFoundResponse('Entity');
333
+ * throw error;
334
+ * }
335
+ */
336
+ function isNotFoundError(error) {
337
+ return (error instanceof Error &&
338
+ 'statusCode' in error &&
339
+ error.statusCode === 404);
340
+ }
341
+ /**
342
+ * Checks if an error is an Azure Table Storage "conflict" error.
343
+ * @param error - The caught error
344
+ * @returns True if error has statusCode 409
345
+ * @example
346
+ * try {
347
+ * await tableClient.createEntity(entity);
348
+ * } catch (error) {
349
+ * if (isConflictError(error)) return conflictResponse('Entity already exists');
350
+ * throw error;
351
+ * }
352
+ */
353
+ function isConflictError(error) {
354
+ return (error instanceof Error &&
355
+ 'statusCode' in error &&
356
+ error.statusCode === 409);
357
+ }
358
+ // =============================================================================
359
+ // Azure Table Storage
360
+ // =============================================================================
361
+ // Cache table clients to avoid repeated connections
362
+ const tableClients = new Map();
363
+ // Configuration
364
+ let storageAccountName;
365
+ let storageConnectionString;
366
+ /**
367
+ * Initializes Azure Table Storage configuration. Call once at application startup.
368
+ * @param config - Storage configuration options
369
+ * @param config.accountName - Azure Storage account name (for managed identity)
370
+ * @param config.connectionString - Connection string (for local development)
371
+ * @example
372
+ * // Production (Azure with managed identity)
373
+ * initStorage({ accountName: 'mystorageaccount' });
374
+ *
375
+ * // Local development (Azurite)
376
+ * initStorage({ connectionString: 'UseDevelopmentStorage=true' });
377
+ */
378
+ function initStorage(config) {
379
+ storageAccountName = config.accountName;
380
+ storageConnectionString = config.connectionString;
381
+ }
382
+ /**
383
+ * Initializes storage from environment variables.
384
+ * Reads STORAGE_ACCOUNT_NAME and AzureWebJobsStorage.
385
+ * @example
386
+ * initStorageFromEnv(); // Uses process.env automatically
387
+ */
388
+ function initStorageFromEnv() {
389
+ initStorage({
390
+ accountName: process.env.STORAGE_ACCOUNT_NAME,
391
+ connectionString: process.env.AzureWebJobsStorage
392
+ });
393
+ }
394
+ /**
395
+ * Checks if using Azure managed identity vs local connection string.
396
+ * @returns True if using managed identity (production), false for connection string (local)
397
+ */
398
+ function useManagedIdentity() {
399
+ return !!storageAccountName && !storageConnectionString?.includes('UseDevelopmentStorage');
400
+ }
401
+ /**
402
+ * Gets a TableClient for the specified table, creating the table if needed.
403
+ * Clients are cached for reuse across requests.
404
+ * @param tableName - The Azure Table Storage table name
405
+ * @returns A configured TableClient instance
406
+ * @throws Error if storage is not configured
407
+ * @example
408
+ * const client = await getTableClient('users');
409
+ * await client.createEntity({ partitionKey: 'pk', rowKey: 'rk', name: 'John' });
410
+ */
411
+ async function getTableClient(tableName) {
412
+ // Return cached client if available
413
+ const cached = tableClients.get(tableName);
414
+ if (cached) {
415
+ return cached;
416
+ }
417
+ let client;
418
+ if (useManagedIdentity()) {
419
+ // Azure: Use managed identity
420
+ const credential = new identity_1.DefaultAzureCredential();
421
+ const endpoint = `https://${storageAccountName}.table.core.windows.net`;
422
+ client = new data_tables_1.TableClient(endpoint, tableName, credential);
423
+ }
424
+ else if (storageConnectionString) {
425
+ // Local/Azurite: Use connection string
426
+ client = data_tables_1.TableClient.fromConnectionString(storageConnectionString, tableName);
427
+ }
428
+ else {
429
+ throw new Error('Storage not configured. Set STORAGE_ACCOUNT_NAME or AzureWebJobsStorage.');
430
+ }
431
+ // Create table if it doesn't exist
432
+ try {
433
+ await client.createTable();
434
+ }
435
+ catch (error) {
436
+ // Ignore "table already exists" errors (409 Conflict)
437
+ const tableError = error;
438
+ if (tableError.statusCode !== 409) {
439
+ throw error;
440
+ }
441
+ }
442
+ // Cache and return
443
+ tableClients.set(tableName, client);
444
+ return client;
445
+ }
446
+ /**
447
+ * Clears the TableClient cache. Primarily useful for testing.
448
+ * @example
449
+ * afterEach(() => {
450
+ * clearTableClientCache();
451
+ * });
452
+ */
453
+ function clearTableClientCache() {
454
+ tableClients.clear();
455
+ }
456
+ /**
457
+ * Generate a unique row key from an identifier string.
458
+ * Uses SHA-256 hash for consistent, URL-safe keys.
459
+ * @param identifier - The string to hash (e.g., subscription endpoint, user ID)
460
+ * @returns A 32-character hex string suitable for Azure Table Storage row keys
461
+ */
462
+ function generateRowKey(identifier) {
463
+ return (0, crypto_1.createHash)('sha256').update(identifier).digest('hex').substring(0, 32);
464
+ }
465
+ // =============================================================================
466
+ // Re-exports from shared (for convenience)
467
+ // =============================================================================
468
+ var shared_2 = require("./shared");
469
+ Object.defineProperty(exports, "ok", { enumerable: true, get: function () { return shared_2.ok; } });
470
+ Object.defineProperty(exports, "okVoid", { enumerable: true, get: function () { return shared_2.okVoid; } });
471
+ Object.defineProperty(exports, "err", { enumerable: true, get: function () { return shared_2.err; } });
472
+ Object.defineProperty(exports, "getErrorMessage", { enumerable: true, get: function () { return shared_2.getErrorMessage; } });
473
+ Object.defineProperty(exports, "hasUsername", { enumerable: true, get: function () { return shared_2.hasUsername; } });
474
+ Object.defineProperty(exports, "hasRole", { enumerable: true, get: function () { return shared_2.hasRole; } });
475
+ Object.defineProperty(exports, "isAdmin", { enumerable: true, get: function () { return shared_2.isAdmin; } });
476
+ Object.defineProperty(exports, "HTTP_STATUS", { enumerable: true, get: function () { return shared_2.HTTP_STATUS; } });
@@ -0,0 +1,150 @@
1
+ /**
2
+ * pwa-core/shared - Types, constants, and utilities for both server and client
3
+ *
4
+ * This module has NO Node.js dependencies and can be safely imported in browsers.
5
+ */
6
+ /**
7
+ * Standard result type for operations that can fail.
8
+ * Used consistently across all modules (auth, push, client).
9
+ *
10
+ * @example
11
+ * // Success
12
+ * { ok: true, data: user }
13
+ *
14
+ * // Failure
15
+ * { ok: false, error: 'Token expired' }
16
+ *
17
+ * // With status code (HTTP/push operations)
18
+ * { ok: false, error: 'Subscription expired', statusCode: 410 }
19
+ */
20
+ export interface Result<T> {
21
+ ok: boolean;
22
+ data?: T;
23
+ error?: string;
24
+ statusCode?: number;
25
+ }
26
+ /**
27
+ * Creates a success result with data.
28
+ * @param data - The success payload
29
+ * @returns A Result with ok=true and the provided data
30
+ * @example
31
+ * return ok({ user: 'john', role: 'admin' });
32
+ */
33
+ export declare function ok<T>(data: T): Result<T>;
34
+ /**
35
+ * Creates a success result with no data.
36
+ * @returns A Result with ok=true and no data
37
+ * @example
38
+ * return okVoid(); // { ok: true }
39
+ */
40
+ export declare function okVoid(): Result<void>;
41
+ /**
42
+ * Creates a failure result with an error message.
43
+ * @param error - The error message
44
+ * @param statusCode - Optional HTTP status code for API/push operations
45
+ * @returns A Result with ok=false and the error details
46
+ * @example
47
+ * return err('Token expired');
48
+ * return err('Subscription gone', 410);
49
+ */
50
+ export declare function err<T>(error: string, statusCode?: number): Result<T>;
51
+ /**
52
+ * Base JWT payload - all tokens include these fields
53
+ * Projects extend this with their specific fields
54
+ */
55
+ export interface BaseJwtPayload {
56
+ iat: number;
57
+ exp: number;
58
+ }
59
+ /**
60
+ * Standard user token payload
61
+ * Used by: azure-pwa-starter, azure-alert-service (admin), onsite-monitor
62
+ */
63
+ export interface UserTokenPayload extends BaseJwtPayload {
64
+ authenticated: true;
65
+ tokenType: 'user' | 'machine';
66
+ }
67
+ /**
68
+ * Username-based token payload
69
+ * Used by: financial-tracker
70
+ */
71
+ export interface UsernameTokenPayload extends BaseJwtPayload {
72
+ username: string;
73
+ }
74
+ /**
75
+ * Role-based token payload
76
+ * Used by: azure-alert-service
77
+ */
78
+ export interface RoleTokenPayload extends BaseJwtPayload {
79
+ authenticated: true;
80
+ tokenType: 'user' | 'machine';
81
+ role: 'admin' | 'viewer';
82
+ viewerTokenId?: string;
83
+ }
84
+ /**
85
+ * Type guard to check if a JWT payload contains a username field.
86
+ * @param payload - The JWT payload to check
87
+ * @returns True if payload has a string username field
88
+ * @example
89
+ * if (hasUsername(payload)) {
90
+ * console.log(payload.username); // TypeScript knows username exists
91
+ * }
92
+ */
93
+ export declare function hasUsername(payload: BaseJwtPayload): payload is UsernameTokenPayload;
94
+ /**
95
+ * Type guard to check if a JWT payload contains a role field.
96
+ * @param payload - The JWT payload to check
97
+ * @returns True if payload has a role field
98
+ * @example
99
+ * if (hasRole(payload)) {
100
+ * console.log(payload.role); // 'admin' | 'viewer'
101
+ * }
102
+ */
103
+ export declare function hasRole(payload: BaseJwtPayload): payload is RoleTokenPayload;
104
+ /**
105
+ * Checks if a JWT payload represents an admin user.
106
+ * @param payload - The JWT payload to check
107
+ * @returns True if payload has role='admin'
108
+ * @example
109
+ * if (!isAdmin(payload)) {
110
+ * return httpForbidden('Admin access required');
111
+ * }
112
+ */
113
+ export declare function isAdmin(payload: BaseJwtPayload): boolean;
114
+ /**
115
+ * HTTP status codes - use instead of magic numbers
116
+ */
117
+ export declare const HTTP_STATUS: {
118
+ readonly OK: 200;
119
+ readonly CREATED: 201;
120
+ readonly NO_CONTENT: 204;
121
+ readonly BAD_REQUEST: 400;
122
+ readonly UNAUTHORIZED: 401;
123
+ readonly FORBIDDEN: 403;
124
+ readonly NOT_FOUND: 404;
125
+ readonly CONFLICT: 409;
126
+ readonly GONE: 410;
127
+ readonly UNPROCESSABLE_ENTITY: 422;
128
+ readonly TOO_MANY_REQUESTS: 429;
129
+ readonly INTERNAL_ERROR: 500;
130
+ readonly SERVICE_UNAVAILABLE: 503;
131
+ };
132
+ export type HttpStatus = typeof HTTP_STATUS[keyof typeof HTTP_STATUS];
133
+ /**
134
+ * Standard error response structure
135
+ */
136
+ export interface ErrorResponse {
137
+ error: string;
138
+ details?: string;
139
+ }
140
+ /**
141
+ * Extracts error message from unknown error, with fallback.
142
+ * @param error - The caught error (unknown type)
143
+ * @param fallback - Default message if error is not an Error instance
144
+ * @returns The error message string
145
+ * @example
146
+ * } catch (error) {
147
+ * return err(getErrorMessage(error, 'Operation failed'));
148
+ * }
149
+ */
150
+ export declare function getErrorMessage(error: unknown, fallback: string): string;