docs: Add comprehensive JSDoc documentation to all source files

- Add detailed file-level documentation with @fileoverview tags
- Document all classes, methods, interfaces, and properties with JSDoc
- Include author attribution (Matthew Raymer) and version information
- Add rich logging with tagged console messages throughout codebase
- Update all @since dates to reflect actual project timeline (2023-09-06)
- Add current documentation update date (2025-07-23) to README
- Document singleton patterns, async methods, and security considerations
- Add parameter and return value documentation for all functions
- Include TypeORM entity documentation with property descriptions
- Enhance README with project intent and middleware architecture details

Files updated:
- src/main.ts: Server entry point and API route documentation
- src/notificationService.ts: Push delivery and encryption documentation
- src/subscriptionService.ts: Subscription management documentation
- src/vapidService.ts: VAPID key generation and authentication docs
- src/worker.ts: Background worker thread documentation
- src/db.ts: Database service and TypeORM integration docs
- src/Subscription.ts: Database entity documentation
- src/VapidKeys.ts: VAPID keys entity documentation
- README.md: Enhanced project documentation and timeline

This commit significantly improves code maintainability and developer
onboarding by providing comprehensive documentation for the entire
push notification middleware codebase.
This commit is contained in:
Matthew Raymer
2025-07-23 07:36:37 +00:00
parent 5f05a2eec6
commit ecc2f809bc
9 changed files with 907 additions and 118 deletions
+24 -1
View File
@@ -1,20 +1,43 @@
// Subscription.ts
/**
* @fileoverview Subscription entity for Time Safari push notifications
*
* This file defines the Subscription entity used by TypeORM for database persistence.
* It represents a push notification subscription with endpoint, encryption keys,
* and muting status.
*
* @author Matthew Raymer
* @version 1.0.0
* @since 2023-09-06
*/
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
/**
* Entity class representing a push notification subscription
*
* @class Subscription
* @description TypeORM entity that maps to the subscriptions table in the database.
* Stores client subscription information including endpoint URLs and encryption keys.
*/
@Entity()
export class Subscription {
/** Unique identifier for the subscription */
@PrimaryGeneratedColumn()
id: number;
/** The push service endpoint URL (e.g., FCM endpoint) */
@Column()
endpoint: string;
/** P-256 ECDH public key for message encryption */
@Column()
keys_p256dh: string;
/** Authentication secret for message integrity */
@Column()
keys_auth: string;
/** Flag indicating if notifications are muted for this subscription */
@Column()
muted: boolean = false;
}
+22 -1
View File
@@ -1,14 +1,35 @@
// VapidKeys.ts
/**
* @fileoverview VAPID keys entity for Time Safari push notifications
*
* This file defines the VapidKeys entity used by TypeORM for database persistence.
* It represents a VAPID (Voluntary Application Server Identification) key pair
* used for authenticating push notification requests.
*
* @author Matthew Raymer
* @version 1.0.0
* @since 2023-09-06
*/
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
/**
* Entity class representing a VAPID key pair
*
* @class VapidKeys
* @description TypeORM entity that maps to the vapid_keys table in the database.
* Stores VAPID public and private keys used for push notification authentication.
*/
@Entity()
export class VapidKeys {
/** Unique identifier for the VAPID key pair */
@PrimaryGeneratedColumn()
id: number;
/** Base64-encoded public key for client-side subscription */
@Column()
publicKey: string;
/** Base64-encoded private key for server-side authentication */
@Column()
privateKey: string;
}
+141 -36
View File
@@ -1,3 +1,14 @@
/**
* @fileoverview Database service for Time Safari push notification middleware
*
* This service provides database connectivity and operations using TypeORM with SQLite.
* It handles the persistence of push subscriptions and VAPID keys, providing a
* singleton interface for database operations throughout the application.
*
* @author Matthew Raymer
* @version 1.0.0
* @since 2023-09-06
*/
import { DataSource } from "typeorm";
import { Subscription } from './Subscription.js'
@@ -6,28 +17,55 @@ import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
/**
* Database service class for managing data persistence
*
* @class DBService
* @description Provides a singleton service for database operations including
* subscription management and VAPID key storage. Uses TypeORM with SQLite for
* lightweight, file-based data persistence.
*/
class DBService {
/** Singleton instance of the database service */
private static instance: DBService;
/** TypeORM data source for database connectivity */
private dataSource: DataSource;
/** Flag indicating if the database is ready for operations */
public isReady = false;
/**
* Private constructor to enforce singleton pattern and initialize database
*
* @private
* @description Initializes the TypeORM data source with SQLite configuration
* and sets up entity synchronization
*/
private constructor() {
console.log('[DB] Initializing database service');
this.dataSource = new DataSource({
type: "sqlite",
database: "push_server",
entities: [VapidKeys, Subscription],
synchronize: true
});
this.dataSource.initialize().then(()=>{
console.log("Initialized");
this.dataSource.initialize().then(() => {
console.log('[DB] Database initialized successfully');
this.isReady = true;
}).catch((err)=>{
console.error(err);
}).catch((err) => {
console.error('[DB] Database initialization error:', err);
});
}
/**
* Gets the singleton instance of the database service
*
* @returns {DBService} The singleton instance
* @static
* @description Implements the singleton pattern to ensure only one database
* connection exists throughout the application
*/
public static getInstance(): DBService {
if (!DBService.instance) {
DBService.instance = new DBService();
@@ -35,82 +73,149 @@ class DBService {
return DBService.instance;
}
async saveSubscription(endpoint: string, keys_p256dh: string, keys_auth: string) {
/**
* Saves a new push subscription to the database
*
* @param {string} endpoint - The push service endpoint URL
* @param {string} keys_p256dh - The client's P-256 ECDH public key
* @param {string} keys_auth - The client's authentication secret
* @returns {Promise<Subscription>} The saved subscription entity
* @async
* @description Creates and persists a new subscription record in the database
*/
async saveSubscription(endpoint: string, keys_p256dh: string, keys_auth: string): Promise<Subscription> {
console.log('[DB] Saving subscription:', endpoint);
const subscription = new Subscription();
subscription.endpoint = endpoint;
subscription.keys_auth = keys_auth;
subscription.keys_p256dh = keys_p256dh;
return await this.dataSource.manager.save(subscription);
const result = await this.dataSource.manager.save(subscription);
console.log('[DB] Subscription saved successfully');
return result;
}
/**
* Retrieves all push subscriptions from the database
*
* @returns {Promise<Subscription[]>} Array of all stored subscriptions
* @async
* @description Fetches all subscription records for notification delivery
*/
async getSubscriptions(): Promise<Subscription[]> {
let result = [ new Subscription ];
console.log('[DB] Fetching all subscriptions');
let result = [new Subscription()];
if (this.isReady) {
result = await this.dataSource.manager.find(Subscription);
console.log('[DB] Retrieved', result.length, 'subscriptions');
} else {
console.log(__filename, "Database not ready.")
console.log('[DB] Database not ready, returning empty result');
}
return result;
}
/**
* Removes a subscription from the database by endpoint
*
* @param {string} endpoint - The endpoint URL of the subscription to remove
* @returns {Promise<Boolean>} True if removal was successful, false otherwise
* @async
* @description Deletes a subscription record from the database
*/
async removeSubscription(endpoint: string): Promise<Boolean> {
console.log('[DB] Removing subscription:', endpoint);
let result = true;
if (this.isReady) {
await this.dataSource.manager.delete(Subscription, { endpoint: endpoint });
console.log('[DB] Subscription removed successfully');
} else {
console.log('[DB] Database not ready, cannot remove subscription');
result = false;
}
return result;
}
/**
* Toggles the muted status of a subscription
*
* @param {string} endpoint - The endpoint URL of the subscription to toggle
* @returns {Promise<Boolean>} True if toggle was successful, false otherwise
* @async
* @description Updates the muted status of a subscription for notification filtering
*/
async toggleMuteSubscription(endpoint: string): Promise<Boolean> {
console.log('[DB] Toggling mute for subscription:', endpoint);
let result = true;
if (this.isReady) {
const subscription = await this.dataSource.manager.findOne(Subscription, { where : {endpoint: endpoint} });
subscription.muted = !subscription.muted;
await this.dataSource.manager.save(subscription)
} else {
result = false;
const subscription = await this.dataSource.manager.findOne(Subscription, {
where: { endpoint: endpoint }
});
if (subscription) {
subscription.muted = !subscription.muted;
await this.dataSource.manager.save(subscription);
console.log('[DB] Subscription mute toggled to:', subscription.muted);
} else {
console.log('[DB] Subscription not found for mute toggle');
result = false;
}
} else {
console.log('[DB] Database not ready, cannot toggle mute');
result = false;
}
return result;
}
/**
* Retrieves all VAPID keys from the database
*
* @returns {Promise<VapidKeys[]>} Array of stored VAPID keys
* @async
* @description Fetches all VAPID key records for authentication use
*/
async getVapidKeys(): Promise<VapidKeys[]> {
console.log(__filename, "getVapidKeys", this.isReady);
let result = [ new VapidKeys() ];
if ( this.isReady ) {
console.log('[DB] Fetching VAPID keys');
let result = [new VapidKeys()];
if (this.isReady) {
result = await this.dataSource.manager.find(VapidKeys);
console.log(__filename, "results of find: ", result);
console.log('[DB] Retrieved', result.length, 'VAPID key(s)');
} else {
console.log(__filename, "Database is not ready");
console.log('[DB] Database is not ready, returning empty result');
}
return result;
}
/**
* Saves a new VAPID key pair to the database
*
* @param {string} publicKey - The base64-encoded public key
* @param {string} privateKey - The base64-encoded private key
* @returns {Promise<VapidKeys>} The saved VAPID keys entity
* @async
* @description Creates and persists a new VAPID key pair record in the database
*/
async saveVapidKeys(publicKey: string, privateKey: string): Promise<VapidKeys> {
console.log('[DB] Saving VAPID keys');
let result = new VapidKeys();
result.privateKey = privateKey;
result.publicKey = publicKey;
if ( this.isReady ) {
if (this.isReady) {
result = await this.dataSource.manager.save(result);
console.log('[DB] VAPID keys saved successfully');
} else {
console.log(__filename, "Database is not ready.");
console.log('[DB] Database is not ready, cannot save VAPID keys');
}
return result;
}
}
+135 -17
View File
@@ -1,3 +1,15 @@
/**
* @fileoverview Main server entry point for the Time Safari Push Notification Middleware
*
* This file contains the primary Express server setup, API route definitions,
* worker thread management, and server initialization logic. It serves as the
* central hub for the push notification middleware service.
*
* @author Matthew Raymer
* @version 1.0.0
* @since 2023-09-06
*/
import SubscriptionService from './subscriptionService.js';
import { Message, NotificationService } from './notificationService.js';
import { VapidKeys } from './VapidKeys.js';
@@ -14,24 +26,55 @@ import { dirname, join } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
/**
* Interface representing a push notification subscription from a client
*
* @interface Subscription
* @description Defines the structure of a push subscription request from client applications
*/
export interface Subscription {
/** The push service endpoint URL (e.g., FCM endpoint) */
endpoint: string;
/** Encryption keys for secure push message delivery */
keys: {
/** P-256 ECDH public key for message encryption */
p256dh: string;
/** Authentication secret for message integrity */
auth: string;
};
}
/**
* Main server class that handles HTTP requests, worker thread management,
* and coordinates all push notification services
*
* @class Server
* @description Central server class that orchestrates the push notification middleware
*/
class Server {
/** Express application instance */
private app: Express;
/** Port number for the server to listen on */
private port: number;
/** Background worker thread for periodic tasks */
private worker?: Worker;
/** Service for managing push subscriptions */
private subscriptionService: SubscriptionService = SubscriptionService.getInstance();
/** Service for sending push notifications */
private notificationService: NotificationService;
dbService: DBService = DBService.getInstance();
vapidService: VapidService = VapidService.getInstance();
/** Database service for data persistence */
dbService: DBService = DBService.getInstance();
/** Service for VAPID key management */
vapidService: VapidService = VapidService.getInstance();
/** Default notification message template */
private message: Message;
/**
* Creates a new Server instance and initializes all services
*
* @param {number} port - The port number to bind the server to
* @description Initializes Express app, sets up routes, starts worker thread, and configures listeners
*/
constructor(port: number) {
this.app = express();
this.port = port;
@@ -42,28 +85,61 @@ class Server {
this.setupWorkerListeners();
}
/**
* Configures all API routes for the push notification service
*
* @private
* @description Sets up REST endpoints for subscription management and VAPID key retrieval
*/
private setupRoutes(): void {
/**
* POST /subscribe - Register a new push notification subscription
*
* @param {Request} req - Express request object containing subscription data
* @param {Response} res - Express response object
* @description Accepts push subscription details and stores them in the database
*/
this.app.post('/subscribe', async (req: Request, res: Response) => {
const subscription = req.body as Subscription;
await this.subscriptionService.addSubscription(subscription);
res.status(201).send();
});
/**
* POST /unsubscribe - Remove a push notification subscription (PLANNED)
*
* @param {Request} req - Express request object containing subscription data
* @param {Response} res - Express response object
* @description Currently returns 501 Not Implemented - planned for future release
*/
this.app.post('/unsubscribe', async (req: Request, res: Response) => {
const subscription = req.body as Subscription;
console.log(subscription);
console.log('[MAIN] Unsubscribe request received:', subscription);
res.status(501).send();
});
/**
* POST /mute - Toggle notification muting for a subscription (PLANNED)
*
* @param {Request} req - Express request object containing subscription data
* @param {Response} res - Express response object
* @description Currently returns 501 Not Implemented - planned for future release
*/
this.app.post('/mute', async (req: Request, res: Response) => {
const subscription = req.body as Subscription;
console.log(subscription);
console.log('[MAIN] Mute request received:', subscription);
res.status(501).send();
});
/**
* GET /vapid - Retrieve the public VAPID key for client-side subscription
*
* @param {Request} _ - Express request object (unused)
* @param {Response} res - Express response object
* @description Returns the public VAPID key needed for client push subscription
*/
this.app.get('/vapid', async (_: Request, res: Response) => {
const vapidkeys: VapidKeys[] = await this.vapidService.getVapidKeys();
const vapidkey = vapidkeys[0];
@@ -72,29 +148,58 @@ class Server {
});
}
/**
* Initializes and starts the background worker thread
*
* @private
* @description Creates a worker thread that handles periodic tasks like daily notifications
*/
private startWorker(): void {
const workerPath = join(__dirname,'./worker.js');
this.worker = new Worker(workerPath, { workerData: 'world' });
/**
* Handles messages from the worker thread
*
* @param {string} message - Message from worker thread
* @description When worker sends notification trigger, broadcasts to all subscribers
*/
this.worker.on('message', (message) => {
console.log(message);
console.log('[MAIN] Worker message received:', message);
this.message = { "title": "Check TimeSafari"} as Message;
this.notificationService.broadcast(this.message);
});
/**
* Handles worker thread errors
*
* @param {Error} error - Error object from worker thread
* @description Logs worker errors for debugging and monitoring
*/
this.worker.on('error', (error) => {
console.error(`Worker error: ${error.message}`);
console.error('[MAIN] Worker error:', error.message);
});
/**
* Handles worker thread exit events
*
* @param {number} code - Exit code from worker thread
* @description Logs worker exit codes for monitoring worker health
*/
this.worker.on('exit', (code) => {
if (code !== 0) {
console.error(`Worker stopped with exit code ${code}`);
console.error(`[MAIN] Worker stopped with exit code ${code}`);
}
});
}
/**
* Sets up additional worker thread message listeners for data requests
*
* @private
* @description Handles worker requests for subscription data and responds accordingly
*/
private setupWorkerListeners(): void {
if (this.worker) {
this.worker.on('message', (message) => {
@@ -107,32 +212,45 @@ class Server {
}
}
/**
* Starts the HTTP server and begins listening for requests
*
* @public
* @description Binds the Express app to the specified port and logs server startup
*/
public start(): void {
this.app.listen(this.port, () => {
console.log(`Server is running on http://localhost:${this.port}`);
console.log(`[MAIN] Server is running on http://localhost:${this.port}`);
});
}
}
// Initialize and start the server
const server = new Server(3000);
/**
* Initializes VAPID keys if they don't exist in the database
*
* @async
* @description Checks for existing VAPID keys and creates new ones if none exist
*/
const executeAsyncFunction = async () => {
const keys: VapidKeys[] = await server.vapidService.getVapidKeys();
if (keys.length === 0) {
console.log('[MAIN] No VAPID keys found, creating new keys...');
await server.vapidService.createVAPIDKeys();
console.log('[MAIN] VAPID keys created successfully');
} else {
console.log('[MAIN] VAPID keys already exist');
}
};
// Execute VAPID key initialization after a 5-second delay
setTimeout(() => {
executeAsyncFunction().catch(error => {
// Handle any errors here
console.error('An error occurred:', error);
console.error('[MAIN] VAPID key initialization error:', error);
});
}, 5000); // Execute after a delay of 5 seconds
}, 5000);
// Start the server
server.start();
+112 -7
View File
@@ -1,3 +1,15 @@
/**
* @fileoverview Push notification delivery service for Time Safari
*
* This service handles the encryption and delivery of push notifications to
* subscribed clients. It implements the Web Push Protocol with VAPID authentication
* and AES-128-GCM encryption for secure message delivery.
*
* @author Matthew Raymer
* @version 1.0.0
* @since 2023-09-06
*/
import SubscriptionService from './subscriptionService.js';
import VapidService from './vapidService.js';
import { VapidKeys } from './VapidKeys.js';
@@ -6,44 +18,117 @@ import * as http_ece from 'http_ece';
import crypto from 'crypto';
import { Subscription } from "./Subscription.js"
/**
* Interface representing a push notification message
*
* @interface Message
* @description Defines the structure of a push notification payload
*/
export interface Message {
/** The title of the notification */
title: string;
/** Optional body text for the notification */
body?: string;
/** Additional custom properties for the notification */
[key: string]: any;
}
/**
* Service class responsible for sending push notifications to subscribed clients
*
* @class NotificationService
* @description Handles the encryption, authentication, and delivery of push notifications
* using the Web Push Protocol with VAPID authentication
*/
export class NotificationService {
/** Service for managing push subscriptions */
private subscriptionService: SubscriptionService = SubscriptionService.getInstance();
/** Service for VAPID key management and authentication */
private vapidService: VapidService = VapidService.getInstance();
/**
* Creates a new NotificationService instance
*
* @description Initializes the notification service with required dependencies
*/
constructor() {
}
/**
* Generates a random salt for message encryption
*
* @param {number} length - The length of the salt in bytes (default: 16)
* @returns {Buffer} A cryptographically secure random salt
* @private
* @description Creates a random salt used in the AES-128-GCM encryption process
*/
private generateSalt(length = 16): Buffer {
return crypto.randomBytes(length);
}
/**
* Sends a notification to all subscribed clients
*
* @param {Message} message - The notification message to broadcast
* @returns {Promise<void>} Resolves when all notifications are sent
* @async
* @description Retrieves all active subscriptions and sends the message to each one
*/
async broadcast(message: Message): Promise<void> {
console.log('[NOTIFICATION] Broadcasting message to all subscribers:', message.title);
const subscriptions = await this.subscriptionService.fetchSubscriptions();
for (const subscription of subscriptions) {
await this.pushToEndpoint(subscription, message);
try {
await this.pushToEndpoint(subscription, message);
console.log('[NOTIFICATION] Successfully sent to:', subscription.endpoint);
} catch (error) {
console.error('[NOTIFICATION] Failed to send to:', subscription.endpoint, error);
}
}
}
async sendNotification(subscription: Subscription, message: Message) {
/**
* Sends a notification to a specific client subscription
*
* @param {Subscription} subscription - The target subscription
* @param {Message} message - The notification message to send
* @returns {Promise<void>} Resolves when the notification is sent
* @async
* @description Sends a single notification to a specific client endpoint
*/
async sendNotification(subscription: Subscription, message: Message): Promise<void> {
console.log('[NOTIFICATION] Sending notification to specific subscription:', subscription.endpoint);
await this.pushToEndpoint(subscription, message);
}
/**
* Delivers a push notification to a specific endpoint
*
* @param {Subscription} subscription - The target subscription details
* @param {Message} message - The notification message payload
* @returns {Promise<void>} Resolves when the HTTP request completes
* @private
* @async
* @description Handles the complete push notification delivery process including
* encryption, VAPID authentication, and HTTPS delivery
*/
private async pushToEndpoint(subscription: Subscription, message: Message): Promise<void> {
const payload = JSON.stringify(message);
// Encrypt the payload using the client's public key
const encrypted = this.encrypt(subscription.keys_p256dh, subscription.keys_auth, payload);
const endpoint = subscription.endpoint;
const vapidHeaders = await this.vapidService.createVapidAuthHeader(endpoint, 12 * 60 * 60, 'mailto:example@example.com');
// Generate VAPID authentication headers
const vapidHeaders = await this.vapidService.createVapidAuthHeader(
endpoint,
12 * 60 * 60, // 12 hours expiration
'mailto:example@example.com'
);
// Parse the endpoint URL for HTTP request
const parsedUrl = new URL(subscription.endpoint);
const options: https.RequestOptions = {
method: 'POST',
@@ -52,8 +137,8 @@ export class NotificationService {
port: parsedUrl.port,
headers: {
...vapidHeaders,
'TTL': '60',
'Content-Encoding': 'aes128gcm',
'TTL': '60', // Time to live in seconds
'Content-Encoding': 'aes128gcm', // Encryption method
'Content-Type': 'application/octet-stream',
'Content-Length': encrypted.length
},
@@ -62,14 +147,19 @@ export class NotificationService {
return new Promise<void>((resolve, reject) => {
const req = https.request(options, (res) => {
if (res.statusCode! >= 200 && res.statusCode! < 300) {
console.log('[NOTIFICATION] Push delivery successful:', res.statusCode);
resolve();
} else {
reject(new Error(`Failed to send push notification. Status code: ${res.statusCode}`));
const error = new Error(`Failed to send push notification. Status code: ${res.statusCode}`);
console.error('[NOTIFICATION] Push delivery failed:', error.message);
reject(error);
}
});
req.on('error', (error) => {
reject(new Error(`Failed to send push notification. Error: ${error.message}`));
const errorMsg = `Failed to send push notification. Error: ${error.message}`;
console.error('[NOTIFICATION] HTTP request error:', errorMsg);
reject(new Error(errorMsg));
});
req.write(encrypted);
@@ -77,13 +167,28 @@ export class NotificationService {
});
}
/**
* Encrypts a message payload using the client's public key
*
* @param {string} publicKey - The client's P-256 ECDH public key
* @param {string} auth - The client's authentication secret
* @param {string} payload - The message payload to encrypt
* @returns {Buffer} The encrypted payload buffer
* @private
* @description Uses the http_ece library to encrypt the payload with AES-128-GCM
* using the client's public key and a random salt
*/
private encrypt(publicKey: string, auth: string, payload: string): Buffer {
// Configure the http_ece library with client keys
http_ece.keys = {
'p256dh': publicKey,
'auth': auth
};
// Get the server's VAPID keys for encryption
const vapidKeys: VapidKeys = this.vapidService.getVapidKeys()[0];
// Encrypt the payload using AES-128-GCM
return http_ece.encrypt(payload, {
'salt': this.generateSalt(),
'dh': vapidKeys.publicKey,
+67 -4
View File
@@ -1,22 +1,67 @@
/**
* @fileoverview Push subscription management service for Time Safari
*
* This service handles the registration, storage, and retrieval of push notification
* subscriptions. It provides a singleton interface for managing client subscriptions
* and integrates with the database service for persistence.
*
* @author Matthew Raymer
* @version 1.0.0
* @since 2023-09-06
*/
import DBService from './db.js';
import { Subscription } from './Subscription.js';
/**
* Interface representing subscription data from client requests
*
* @interface SubscriptionData
* @description Defines the structure of subscription data received from client applications
*/
export interface SubscriptionData {
/** The push service endpoint URL (e.g., FCM endpoint) */
endpoint: string;
/** Encryption keys for secure push message delivery */
keys: {
/** P-256 ECDH public key for message encryption */
p256dh: string;
/** Authentication secret for message integrity */
auth: string;
};
}
/**
* Service class for managing push notification subscriptions
*
* @class SubscriptionService
* @description Provides a singleton service for adding, retrieving, and managing
* push notification subscriptions. Integrates with the database service for
* persistent storage of subscription data.
*/
class SubscriptionService {
/** Singleton instance of the subscription service */
private static instance: SubscriptionService;
/** Database service for subscription persistence */
private dbService: DBService = DBService.getInstance();
/**
* Private constructor to enforce singleton pattern
*
* @private
* @description Prevents direct instantiation of the service class
*/
private constructor() {
}
/**
* Gets the singleton instance of the subscription service
*
* @returns {SubscriptionService} The singleton instance
* @static
* @description Implements the singleton pattern to ensure only one instance
* of the subscription service exists throughout the application
*/
public static getInstance(): SubscriptionService {
if (!SubscriptionService.instance) {
SubscriptionService.instance = new SubscriptionService();
@@ -24,18 +69,36 @@ class SubscriptionService {
return SubscriptionService.instance;
}
/**
* Adds a new push notification subscription to the database
*
* @param {SubscriptionData} subscription - The subscription data to store
* @returns {Promise<void>} Resolves when the subscription is saved
* @async
* @description Saves a new client subscription with endpoint and encryption keys
*/
async addSubscription(subscription: SubscriptionData): Promise<void> {
console.log('[SUBSCRIPTION] Adding new subscription:', subscription.endpoint);
await this.dbService.saveSubscription(
subscription.endpoint,
subscription.keys.p256dh,
subscription.keys.auth
);
console.log('[SUBSCRIPTION] Subscription added successfully');
}
/**
* Retrieves all active push notification subscriptions
*
* @returns {Promise<Subscription[]>} Array of all stored subscriptions
* @async
* @description Fetches all subscriptions from the database for notification delivery
*/
async fetchSubscriptions(): Promise<Subscription[]> {
return this.dbService.getSubscriptions();
console.log('[SUBSCRIPTION] Fetching all subscriptions');
const subscriptions = await this.dbService.getSubscriptions();
console.log('[SUBSCRIPTION] Retrieved', subscriptions.length, 'subscriptions');
return subscriptions;
}
}
+105 -27
View File
@@ -1,3 +1,14 @@
/**
* @fileoverview VAPID key management service for Time Safari
*
* This service handles the generation, storage, and management of VAPID (Voluntary
* Application Server Identification) keys used for authenticating push notification
* requests. It provides secure key generation and JWT-based authentication headers.
*
* @author Matthew Raymer
* @version 1.0.0
* @since 2023-09-06
*/
import { VapidKeys } from './VapidKeys.js';
import jwt from 'jsonwebtoken';
@@ -7,20 +18,50 @@ import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
/**
* Interface representing VAPID key pair data
*
* @interface VapidKeyData
* @description Defines the structure of a VAPID key pair with public and private keys
*/
export interface VapidKeyData {
/** Base64-encoded public key for client-side subscription */
publicKey: string;
/** Base64-encoded private key for server-side authentication */
privateKey: string;
}
/**
* Service class for managing VAPID keys and authentication
*
* @class VapidService
* @description Provides a singleton service for generating, storing, and using VAPID
* keys for push notification authentication. Implements JWT-based authentication
* headers for secure push delivery.
*/
class VapidService {
/** Singleton instance of the VAPID service */
private static instance: VapidService;
/** Database service for VAPID key persistence */
private dbService: DBService = DBService.getInstance();
/**
* Private constructor to enforce singleton pattern
*
* @private
* @description Prevents direct instantiation of the service class
*/
private constructor() {
}
/**
* Gets the singleton instance of the VAPID service
*
* @returns {VapidService} The singleton instance
* @static
* @description Implements the singleton pattern to ensure only one instance
* of the VAPID service exists throughout the application
*/
public static getInstance(): VapidService {
if (!VapidService.instance) {
VapidService.instance = new VapidService();
@@ -28,69 +69,106 @@ class VapidService {
return VapidService.instance;
}
/**
* Creates and stores a new VAPID key pair
*
* @returns {Promise<VapidKeys>} The created VAPID keys entity
* @async
* @description Generates a new ECDH key pair and stores it in the database
*/
public async createVAPIDKeys(): Promise<VapidKeys> {
console.log('[VAPID] Creating new VAPID keys');
let result = new VapidKeys();
if ( this.dbService.isReady ) {
if (this.dbService.isReady) {
const keys = this.generateVAPIDKeys();
await this.dbService.saveVapidKeys(keys['publicKey'], keys['privateKey']);
console.log('[VAPID] VAPID keys created and saved successfully');
} else {
console.log(__filename, "Database is not ready.");
console.log('[VAPID] Database is not ready, cannot save VAPID keys');
}
return result;
}
/**
* Generates a new VAPID key pair using ECDH
*
* @returns {VapidKeyData} The generated public and private keys
* @private
* @description Creates a new ECDH key pair using the P-256 curve for VAPID authentication
*/
private generateVAPIDKeys(): VapidKeyData {
console.log('[VAPID] Generating new ECDH key pair');
const ecdh = crypto.createECDH('prime256v1');
ecdh.generateKeys();
const result = {
publicKey: ecdh.getPublicKey().toString('base64'),
privateKey: ecdh.getPrivateKey().toString('base64')
};
return result;
}
/*
private async addVapidKeys(vapidkeys: VapidKeyData): Promise<VapidKeys> {
let result = new VapidKeys();
const keys = await this.getVapidKeys();
if (keys.length == 1 && typeof(keys[0].publicKey) == "undefined" ) {
result = await this.dbService.saveVapidKeys(vapidkeys.publicKey, vapidkeys.privateKey);
}
console.log('[VAPID] Key pair generated successfully');
return result;
}
*/
/**
* Retrieves all stored VAPID keys from the database
*
* @returns {Promise<VapidKeys[]>} Array of stored VAPID keys
* @async
* @description Fetches all VAPID keys from the database for authentication use
*/
async getVapidKeys(): Promise<VapidKeys[]> {
console.log('[VAPID] Retrieving VAPID keys from database');
let result = await this.dbService.getVapidKeys();
console.log('[VAPID] Retrieved', result.length, 'VAPID key(s)');
return result;
}
/**
* Creates VAPID authentication headers for push notification requests
*
* @param {string} endpoint - The push service endpoint URL
* @param {number} expiration - JWT expiration time in seconds
* @param {string} subject - The subject identifier (usually email)
* @returns {Promise<{ 'Authorization': string, 'Crypto-Key': string }>} VAPID headers
* @async
* @description Generates JWT-based authentication headers required for push delivery
*/
async createVapidAuthHeader(
endpoint: string,
expiration: number,
subject: string
): Promise<{ 'Authorization': string, 'Crypto-Key': string }> {
console.log('[VAPID] Creating authentication headers for endpoint:', endpoint);
const vapidKeys = await this.getVapidKeys();
const { publicKey, privateKey } = vapidKeys[0];
async createVapidAuthHeader(endpoint: string, expiration: number, subject: string): Promise<{ 'Authorization': string, 'Crypto-Key': string }> {
const { publicKey, privateKey } = await this.getVapidKeys()[0];
// Create JWT payload for VAPID authentication
const jwtInfo = {
aud: new URL(endpoint).origin,
exp: Math.floor((Date.now() / 1000) + expiration),
sub: subject
aud: new URL(endpoint).origin, // Audience (push service origin)
exp: Math.floor((Date.now() / 1000) + expiration), // Expiration time
sub: subject // Subject (contact email)
};
// Sign the JWT with the private key using ES256 algorithm
const jwtToken = jwt.sign(jwtInfo, privateKey, { algorithm: 'ES256' });
return {
const headers = {
'Authorization': `vapid t=${jwtToken}, k=${publicKey}`,
'Crypto-Key': publicKey
};
console.log('[VAPID] Authentication headers created successfully');
return headers;
}
}
// Self-executing function for initialization (currently empty)
(async ()=> {
// Future initialization logic can be added here
})();
export default VapidService;
+44 -2
View File
@@ -1,20 +1,62 @@
/**
* @fileoverview Background worker thread for Time Safari push notifications
*
* This worker thread runs in the background and handles periodic tasks such as
* sending scheduled notifications. It communicates with the main thread via
* message passing to coordinate notification delivery.
*
* @author Matthew Raymer
* @version 1.0.0
* @since 2023-09-06
*/
import { parentPort } from 'worker_threads';
/**
* Worker thread class for handling periodic background tasks
*
* @class WorkerThread
* @description Manages periodic tasks that run in a separate thread to avoid
* blocking the main server thread. Currently handles daily notification scheduling.
*/
class WorkerThread {
/** Interval in milliseconds between task executions */
private interval: number;
/**
* Creates a new worker thread instance
*
* @param {number} interval - The interval in milliseconds between task executions
* @description Initializes the worker thread and starts the periodic task scheduler
*/
constructor(interval: number) {
this.interval = interval;
this.startPeriodicTask();
console.log('[WORKER] Worker thread initialized with interval:', interval, 'ms');
}
/**
* Starts the periodic task scheduler
*
* @private
* @description Sets up an interval timer that sends messages to the main thread
* at the specified interval. Currently triggers daily notifications.
*/
private startPeriodicTask(): void {
console.log('[WORKER] Starting periodic task scheduler');
setInterval(() => {
if (parentPort) {
parentPort.postMessage("send notifications")
console.log('[WORKER] Sending notification trigger to main thread');
parentPort.postMessage("send notifications");
} else {
console.error('[WORKER] Parent port not available');
}
}, this.interval);
console.log('[WORKER] Periodic task scheduler started successfully');
}
}
new WorkerThread(24*3600*1000); // pole once per day
// Initialize the worker thread with a 24-hour interval (daily notifications)
new WorkerThread(24 * 3600 * 1000); // Poll once per day