diff --git a/README.md b/README.md index 57ec370..6d4877f 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ ## Motivation -HyperExpress.Session aims to provide a simple middleware that implements a session engine with high flexibility while being performant. This middleware is unopinionated and can be used with any storage mechanism of your choice. +HyperExpress.Session aims to provide a simple middleware that implements a session engine with high flexibility while being performant. This middleware is unopinionated and can be used with any storage mechanism of your choice. This middleware ships with TypeScript types out of the box, so both vanilla Javascript and TypeScript projects are welcome. ## Installation HyperExpress and HyperExpressWS can both be installed using node package manager (`npm`) diff --git a/package-lock.json b/package-lock.json index fd2596e..59634e3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,49 +1,82 @@ { - "name": "hyper-express-session", - "version": "1.0.3", - "lockfileVersion": 2, - "requires": true, - "packages": { - "": { - "version": "1.0.3", - "license": "MIT", - "dependencies": { - "uid-safe": "^2.1.5" - } + "name": "hyper-express-session", + "version": "1.0.3", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "version": "1.0.3", + "license": "MIT", + "dependencies": { + "@types/node": "^16.11.6", + "uid-safe": "^2.1.5" + }, + "devDependencies": { + "typescript": "^4.4.4" + } + }, + "node_modules/@types/node": { + "version": "16.11.21", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.21.tgz", + "integrity": "sha512-Pf8M1XD9i1ksZEcCP8vuSNwooJ/bZapNmIzpmsMaL+jMI+8mEYU3PKvs+xDNuQcJWF/x24WzY4qxLtB0zNow9A==" + }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha1-T2ih3Arli9P7lYSMMDJNt11kNgs=", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/typescript": { + "version": "4.5.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.5.tgz", + "integrity": "sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + } }, - "node_modules/random-bytes": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", - "integrity": "sha1-T2ih3Arli9P7lYSMMDJNt11kNgs=", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/uid-safe": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", - "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", - "dependencies": { - "random-bytes": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - } - }, - "dependencies": { - "random-bytes": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", - "integrity": "sha1-T2ih3Arli9P7lYSMMDJNt11kNgs=" - }, - "uid-safe": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", - "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", - "requires": { - "random-bytes": "~1.0.0" - } + "dependencies": { + "@types/node": { + "version": "16.11.21", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.21.tgz", + "integrity": "sha512-Pf8M1XD9i1ksZEcCP8vuSNwooJ/bZapNmIzpmsMaL+jMI+8mEYU3PKvs+xDNuQcJWF/x24WzY4qxLtB0zNow9A==" + }, + "random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha1-T2ih3Arli9P7lYSMMDJNt11kNgs=" + }, + "typescript": { + "version": "4.5.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.5.tgz", + "integrity": "sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==", + "dev": true + }, + "uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "requires": { + "random-bytes": "~1.0.0" + } + } } - } } diff --git a/src/components/Session.js b/src/components/Session.js index b1cf3b2..53d3a3f 100644 --- a/src/components/Session.js +++ b/src/components/Session.js @@ -30,7 +30,7 @@ class Session { /** * This method asynchronously generates a strong cryptographically random session id. * - * @returns {Promise} Promise -> String + * @returns {Promise} */ async generate_id() { return await this.#session_engine.methods.id(); @@ -57,7 +57,7 @@ class Session { * unsign the the provided id and thus verifies user input. * * @param {String} signed_id Signed Session ID - * @param {String} secret Optional (Utilizes SessionEngine.options.cookie.secret by default) + * @param {String=} secret Optional (Utilizes SessionEngine.options.cookie.secret by default) * @returns {Boolean} */ set_signed_id(signed_id, secret) { @@ -96,7 +96,7 @@ class Session { * This method is used to start a session for incoming request. * Note! This method is asynchronous as it performs the 'read' operation to read session data. * - * @returns {Promise} Promise + * @returns {Promise} */ async start() { // Return if session has already started @@ -143,7 +143,7 @@ class Session { * Note! This operation performs 2 underlying operations as it first * deletes old session and then persists session data under new session id. * - * @returns {Promise} Promise -> Boolean (true || false) + * @returns {Promise} */ async roll() { // Throw not started error if session was not started/ready @@ -165,7 +165,7 @@ class Session { /** * This method performs the 'touch' operation updating session's expiry in storage. * - * @returns {Promise} Promise + * @returns {Promise} */ touch() { // Return if no session cookie was sent with request @@ -371,6 +371,7 @@ class Session { /** * Returns whether session is ready and its data has been retrieved. + * @returns {Boolean} */ get ready() { return this.#ready; @@ -379,6 +380,7 @@ class Session { /** * Returns whether session has already been stored in database or not. * This is helpful for choosing between INSERT/UPDATE operations for SQL based implementations. + * @returns {Boolean} */ get stored() { return this.#from_database; @@ -386,6 +388,7 @@ class Session { /** * Returns the current session's lifetime duration in milliseconds. + * @returns {Number} */ get duration() { const default_duration = this.#session_engine.options.duration; @@ -394,7 +397,8 @@ class Session { } /** - * Returns the expiry unix timestamp in milliseconds of current session. + * Returns the expiry UNIX timestamp in milliseconds of current session. + * @returns {Number} */ get expires_at() { return Date.now() + this.duration; diff --git a/src/components/SessionEngine.js b/src/components/SessionEngine.js index ab7e7a4..51c37ca 100644 --- a/src/components/SessionEngine.js +++ b/src/components/SessionEngine.js @@ -40,9 +40,7 @@ class SessionEngine { // Ensure the session duration is a valid number const duration = this.#options.duration; if (typeof duration !== 'number' || duration < 1) - throw new Error( - 'new SessionEngine(options.duration) -> duration must be a valid number in milliseconds.' - ); + throw new Error('new SessionEngine(options.duration) -> duration must be a valid number in milliseconds.'); // Ensure user has specified a secret as it is required const secret = this.#options.cookie.secret; @@ -86,14 +84,12 @@ class SessionEngine { * * @param {String} type [id, touch, read, write, destroy] * @param {function(Session):void} handler - * @returns {SessionEngine} + * @returns {SessionEngine} SessionEngine (Chainable) */ use(type, handler) { // Ensure type is valid string that is supported if (typeof type !== 'string' || this.#methods[type] == undefined) - throw new Error( - 'SessionEngine.use(type, handler) -> type must be a string that is a supported operation.' - ); + throw new Error('SessionEngine.use(type, handler) -> type must be a string that is a supported operation.'); // Ensure handler is an executable function if (typeof handler !== 'function') @@ -105,7 +101,7 @@ class SessionEngine { } /** - * Triggers 'cleanup' operation. + * Triggers 'cleanup' operation based on the assigned "cleanup" handler. */ cleanup() { return this.#methods.cleanup(); @@ -121,7 +117,7 @@ class SessionEngine { } /** - * SessionEngine operation methods. + * SessionEngine assigned operation method handlers. */ get methods() { return this.#methods; @@ -129,6 +125,7 @@ class SessionEngine { /** * SessionEngine middleware function to be passed into HyperExpress.use() method. + * @returns {Function} */ get middleware() { return this.#middleware; diff --git a/types/components/Session.d.ts b/types/components/Session.d.ts new file mode 100644 index 0000000..44c2169 --- /dev/null +++ b/types/components/Session.d.ts @@ -0,0 +1,157 @@ +interface SessionData { + [key: string]: any; +} + +export default class Session { + /* Session Methods */ + + /** + * This method asynchronously generates a strong cryptographically random session id. + * + * @returns {Promise} + */ + generate_id(): Promise; + + /** + * This method sets the current session's id to provided session_id. + * Note! This method does not perform any verification on provided session_id + * and thus is not recommended to be used with any user a provided id. + * + * @param {String} id + * @returns {Session} Session (chainable) + */ + set_id(session_id: string): Session; + + /** + * This method sets the current session's id to provided signed session id. + * Note! This method is recommended over .set_id() as this method will attempt to + * unsign the the provided id and thus verifies user input. + * + * @param {String} signed_id Signed Session ID + * @param {String=} secret Optional (Utilizes SessionEngine.options.cookie.secret by default) + * @returns {Boolean} + */ + set_signed_id(signed_id: string, secret?: string): boolean; + + /** + * This method is used to change the duration of current session to a custom value in milliseconds. + * + * @param {Number} duration In Milliseconds + * @returns {Session} Session (Chainable) + */ + set_duration(duration: number): Session; + + /** + * This method is used to start a session for incoming request. + * Note! This method is asynchronous as it performs the 'read' operation to read session data. + * + * @returns {Promise} + */ + start(): Promise; + + /** + * Rolls current session's id to a new session id. + * Note! This operation performs 2 underlying operations as it first + * deletes old session and then persists session data under new session id. + * + * @returns {Promise} + */ + roll(): Promise; + + /** + * This method performs the 'touch' operation updating session's expiry in storage. + * + * @returns {Promise} + */ + touch(): Promise; + + /** + * This method is used to destroy the current session. + * Note! This method is asynchronous as it instantly triggers + * the 'destroy' operation causing session to be deleted from storage mechanism. + * + * @returns {Promise} + */ + destroy(): Promise; + + /** + * This method is used to set one or multiple session data values. + * You may provide a name and value argument to update a single value. + * You may provide an Object of keys/values to update multiple values in one operation. + * + * @param {String} name + * @param {Any} value + * @returns {Session} Session (Chainable) + */ + set(name: string, value: any): Session; + set(values: SessionData): Session; + + /** + * This method replaces current session data values with provided data values object. + * + * @param {SessionData} data + * @returns {Session} Session (Chainable) + */ + reset(data: SessionData): Session; + + /** + * This method is used to retrieve data values from current session. + * You may retrieve all session data values by providing no name parameter. + * + * @param {String=} name Optional + * @returns {Any|Object|undefined} + */ + get(name: string): any | void; + get(): SessionData; + + /** + * This method is used to delete data values from current session. + * You may delete all session data values by providing no name. + * + * @param {String} name + * @returns {Session} Session (Chainable) + */ + delete(name: string): Session; + delete(): Session; + + /* Session Getters */ + + /** + * Parses and returns session id from current request based on session cookie. + * + * @returns {String|void} + */ + get id(): string | void; + + /** + * This method is an alias of .id() except it returns the raw signed id. + * + * @returns {String|undefined} + */ + get signed_id(): string | void; + + /** + * Returns whether session is ready and its data has been retrieved. + * @returns {Boolean} + */ + get ready(): boolean; + + /** + * Returns whether session has already been stored in database or not. + * This is helpful for choosing between INSERT/UPDATE operations for SQL based implementations. + * @returns {Boolean} + */ + get stored(): boolean; + + /** + * Returns the current session's lifetime duration in milliseconds. + * @returns {Number} + */ + get duration(): number; + + /** + * Returns the expiry UNIX timestamp in milliseconds of current session. + * @returns {Number} + */ + get expires_at(): number; +} \ No newline at end of file diff --git a/types/components/SessionEngine.d.ts b/types/components/SessionEngine.d.ts new file mode 100644 index 0000000..ec14f78 --- /dev/null +++ b/types/components/SessionEngine.d.ts @@ -0,0 +1,45 @@ +import Session from './Session'; + +type EngineHandler = (session: Session) => void | Promise; + +interface EngineMethods { + [name: string]: Function +} + +export default class SessionEngine { + /* SessionEngine Methods */ + + /** + * This method is used to specify a handler for session engine operations. + * + * @param {String} type [id, touch, read, write, destroy] + * @param {EngineHandler} handler + * @returns {SessionEngine} SessionEngine (Chainable) + */ + use(type: string, handler: EngineHandler): SessionEngine; + + /** + * Triggers 'cleanup' operation based on the assigned "cleanup" handler. + */ + cleanup(): void; + + /* SessionEngine Getters */ + + /** + * SessionEngine constructor options. + * @returns {Object} + */ + get options(): object; + + /** + * SessionEngine assigned operation method handlers. + * @returns {EngineMethods} + */ + get methods(): EngineMethods; + + /** + * SessionEngine middleware function to be passed into HyperExpress.use() method. + * @returns {Function} + */ + get middleware(): Function; +} \ No newline at end of file diff --git a/types/index.d.ts b/types/index.d.ts new file mode 100644 index 0000000..31887ae --- /dev/null +++ b/types/index.d.ts @@ -0,0 +1,2 @@ +import SessionEngine from "./components/SessionEngine"; +export default SessionEngine; \ No newline at end of file