diff --git a/package.json b/package.json index 0129e61..ec942e8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@blinkclaud/octobus", - "version": "0.6.1", + "version": "0.7.0", "description": "A toolkit for Blink HQ's microservices", "author": "Blink HQ", "private": false, @@ -69,6 +69,7 @@ "@types/express": "^4.17.17", "@types/jest": "^29.5.2", "@types/lodash": "^4.17.7", + "@types/ms": "^0.7.34", "@types/node": "^20.3.1", "@types/node-cron": "^3.0.11", "@types/request-ip": "^0.0.41", diff --git a/src/tokens/redis.store.ts b/src/tokens/redis.store.ts new file mode 100644 index 0000000..b16ae64 --- /dev/null +++ b/src/tokens/redis.store.ts @@ -0,0 +1,70 @@ +import crypto from "crypto"; + +import { Redis } from "ioredis"; +import ms from "ms"; + +import { dateReviver } from "../strings"; +import { AsyncNullable, TokenStore } from "./store"; + +export class RedisStore implements TokenStore { + constructor(private secret: string, private redis: Redis) {} + + async commision(key: string, val: T, time: string): Promise { + const token = crypto.createHmac("sha256", this.secret).update(key).digest("hex"); + + const content = JSON.stringify(val); + await this.redis.set(token, content, "PX", ms(time)); + + return token; + } + + async peek(token: string): AsyncNullable { + const result = await this.redis.get(token); + if (!result) { + return null; + } + + return JSON.parse(result, dateReviver); + } + + async extend(token: string, time: string): AsyncNullable { + const result = await this.redis.get(token); + if (!result) { + return null; + } + + await this.redis.pexpire(token, ms(time)); + + return JSON.parse(result, dateReviver); + } + + async reset(key: string, newVal: T): Promise { + const token = crypto.createHmac("sha256", this.secret).update(key).digest("hex"); + + // make sure the token exists + const result = await this.redis.get(token); + if (!result) return; + + const content = JSON.stringify(newVal); + const ttl = await this.redis.pttl(token); + + await this.redis.set(token, content, "PX", ttl); + } + + async decommission(token: string): AsyncNullable { + const result = await this.redis.get(token); + if (!result) { + return null; + } + + await this.redis.del(token); + + return JSON.parse(result, dateReviver); + } + + async revoke(key: string): Promise { + const token = crypto.createHmac("sha256", this.secret).update(key).digest("hex"); + + await this.redis.del(token); + } +} diff --git a/src/tokens/store.ts b/src/tokens/store.ts new file mode 100644 index 0000000..f9e99b2 --- /dev/null +++ b/src/tokens/store.ts @@ -0,0 +1,45 @@ +export type AsyncNullable = Promise; + +/** + * Contract of stores used to manage single use tokens. What's most important of + * said tokens(vs JWT for instance) is the ability to revoke said token. + */ +export interface TokenStore { + /** + * Create a single use token that expires after the given timeout + * @param key key to enabled reset and revoke + * @param val value the token will refer to + * @param time timeout before expiry + */ + commision(key: string, val: T, time: string): Promise; + /** + * Get the data the token references without changing its lifetime + * @param token token to check for + */ + peek(token: string): AsyncNullable; + /** + * Set the new duration before an existing token times out. Note that it doesn't + * take into account how long the old token had to expire, as it uses the new duration + * entirely. + * @param token generated token + * @param time the new expiry duration of the token + */ + extend(token: string, time: string): AsyncNullable; + /** + * Change the contents of the token without changing it's TTL + * @param key key used to generate the token + * @param newVal value to replace token content + */ + reset(key: string, newVal: T): Promise; + /** + * Load the value referenced by the token and dispenses of the token, + * making it unvailable for further use. + * @param token token to be decomissioned + */ + decommission(token: string): AsyncNullable; + /** + * Render the token generated for the given key useless. + * @param key key used to generate the token + */ + revoke(key: string): Promise; +} diff --git a/yarn.lock b/yarn.lock index 28056db..9b903af 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1251,6 +1251,11 @@ resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690" integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== +"@types/ms@^0.7.34": + version "0.7.34" + resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.34.tgz#10964ba0dee6ac4cd462e2795b6bebd407303433" + integrity sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g== + "@types/node-cron@^3.0.11": version "3.0.11" resolved "https://registry.yarnpkg.com/@types/node-cron/-/node-cron-3.0.11.tgz#70b7131f65038ae63cfe841354c8aba363632344"