ggez-banking-sdk
Version:
A Node.js package to handle GGEZ Banking API endpoints, Simplify the process of managing CRUD operations with this efficient and easy-to-use package.
664 lines (663 loc) • 22.7 kB
JavaScript
import { v4 } from "uuid";
import DocumentCookie from "js-cookie";
import { CipherHelper } from "../cipherHelper";
class CookiesHelper {
// #region "Properties"
key;
pendingIID = null;
eventHandlers = new Map();
domain = undefined;
programId = "0";
Cookies = null;
useCookieStore = false;
cipherHelper;
errorHandler;
cachedIID = null;
cachedUSR = null;
cachedDEK = null;
cachedAccessToken = null;
cachedJWTToken = null;
// #endregion
// #region "Constructor"
constructor(programId, domain, errorHandler) {
this.key = v4();
this.programId = programId;
this.errorHandler = errorHandler;
this.domain = domain;
this.cipherHelper = new CipherHelper(this.errorHandler);
// Check if cookieStore API is available (requires HTTPS or localhost)
this.useCookieStore = this.isCookieStoreAvailable();
if (this.useCookieStore && window.cookieStore) {
this.Cookies = window.cookieStore;
}
}
// #endregion
// #region "Utils"
/**
* Check if CookieStore API is available
* CookieStore requires secure context (HTTPS or localhost)
*/
isCookieStoreAvailable() {
return (typeof window !== "undefined" &&
"cookieStore" in window &&
window.cookieStore !== null &&
window.cookieStore !== undefined);
}
isSecureContext() {
if (typeof window === "undefined")
return false;
return window.isSecureContext;
}
buildCookieName(key) {
return `${key}-${this.programId}`;
}
clearCacheForKey(cookieName) {
switch (cookieName) {
case `IID-${this.programId}`:
this.cachedIID = null;
this.cachedUSR = null;
this.cachedDEK = null;
this.cachedAccessToken = null;
break;
case `USR-${this.programId}`:
this.cachedUSR = null;
this.cachedDEK = null;
break;
case `DEK-${this.programId}`:
this.cachedDEK = null;
break;
case `access_token-${this.programId}`:
this.cachedAccessToken = null;
break;
case `jwt_token-${this.programId}`:
this.cachedJWTToken = null;
break;
}
}
// #endregion
// #region "Fallback Methods"
setCookieFallback(name, value, expires, domain) {
const isSecure = this.isSecureContext();
DocumentCookie.set(name, value, {
expires: expires ? new Date(expires) : undefined,
path: "/",
domain: domain,
secure: isSecure,
sameSite: isSecure ? "none" : "lax",
});
}
getCookieFallback(name) {
if (typeof document === "undefined")
return "";
return DocumentCookie.get(name) || "";
}
deleteCookieFallback(name, domain) {
DocumentCookie.remove(name, {
path: "/",
domain: domain,
});
}
// #endregion
// #region "IID"
async getIID() {
if (this.cachedIID)
return this.cachedIID;
if (this.pendingIID)
return this.pendingIID;
this.pendingIID = this._fetchOrCreateIID().finally(() => {
this.pendingIID = null;
});
return this.pendingIID;
}
async _fetchOrCreateIID() {
try {
const installationId = await this.get("IID");
if (!installationId) {
return await this.setIID();
}
const { key, iv } = this.cipherHelper.generateByProgramID(this.programId);
const { IID } = this.cipherHelper.decryptAsJson(installationId, key, iv);
if (!IID) {
await this.remove("IID");
return await this.setIID();
}
this.cachedIID = IID;
return IID;
}
catch (error) {
this.errorHandler(error);
return "";
}
}
async setIID() {
try {
const IID = v4();
const { key, iv } = this.cipherHelper.generateByProgramID(this.programId);
const encryptedIID = this.cipherHelper.encryptAsJson(IID, "IID", key, iv);
await this.set("IID", encryptedIID);
this.cachedIID = IID;
return IID;
}
catch (error) {
this.errorHandler(error);
return "";
}
}
async validateIID(installationId) {
try {
const { key, iv } = this.cipherHelper.generateByProgramID(this.programId);
const { IID } = this.cipherHelper.decryptAsJson(installationId, key, iv);
return !!IID;
}
catch (error) {
this.errorHandler(error);
return false;
}
}
// #endregion
// #region "DEK"
async getDEK() {
try {
if (this.cachedDEK)
return this.cachedDEK;
const [IID, DEK, USR] = await Promise.all([
this.getIID(),
this.get("DEK"),
this.getUSR(),
]);
if (!DEK || !USR || !IID) {
return "";
}
const { key, iv } = this.cipherHelper.generateByUSRAndIID(USR, IID);
const { DEK: encryptionKey } = this.cipherHelper.decryptAsJson(DEK, key, iv);
if (encryptionKey.length == 16) {
this.cachedDEK = encryptionKey;
return encryptionKey;
}
return "";
}
catch (error) {
this.errorHandler(error);
return "";
}
}
async setDEK(deviceEncryptionKey, USR) {
try {
const IID = await this.getIID();
if (!USR || !IID)
return;
const { user_id } = USR;
const { key, iv } = this.cipherHelper.generateByUSRAndIID(USR, IID);
if (deviceEncryptionKey) {
const cipherKeys = this.cipherHelper.generateByUserID(user_id);
const encryptionKey = this.cipherHelper.decrypt(deviceEncryptionKey, cipherKeys.key, cipherKeys.iv);
const DEK = this.cipherHelper.encryptAsJson(encryptionKey, "DEK", key, iv);
await this.set("DEK", DEK);
if (encryptionKey.length === 16) {
this.cachedDEK = encryptionKey;
}
}
}
catch (error) {
this.errorHandler(error);
}
}
async validateDEK(DEK) {
try {
const IID = this.cachedIID;
const USR = this.cachedUSR;
if (!DEK || !USR || !IID)
return false;
const { key, iv } = this.cipherHelper.generateByUSRAndIID(USR, IID);
const { DEK: decryptedDEK } = this.cipherHelper.decryptAsJson(DEK, key, iv);
return !!decryptedDEK;
}
catch (error) {
this.errorHandler(error);
return false;
}
}
// #endregion
// #region "User ID / Device ID"
async getUserId() {
try {
const USR = await this.getUSR();
return Number(USR?.user_id || 0);
}
catch (error) {
this.errorHandler(error);
return 0;
}
}
async getDeviceId() {
try {
const USR = await this.getUSR();
return Number(USR?.device_id || 0);
}
catch (error) {
this.errorHandler(error);
return 0;
}
}
// #endregion
// #region "USR"
async getUSR() {
try {
if (this.cachedUSR)
return this.cachedUSR;
const [IID, USR] = await Promise.all([this.getIID(), this.get("USR")]);
if (!IID || !USR)
return null;
const { key, iv } = this.cipherHelper.generateByInstallationID(IID);
const { USR: decryptedUSR } = this.cipherHelper.decryptAsJson(USR, key, iv);
if (!decryptedUSR)
return null;
const parsed = JSON.parse(decryptedUSR);
this.cachedUSR = parsed;
return parsed;
}
catch (error) {
this.errorHandler(error);
return null;
}
}
async setUSR(deviceId, userId) {
try {
const IID = await this.getIID();
if (!IID)
return;
const { key, iv } = this.cipherHelper.generateByInstallationID(IID);
const usr = { device_id: deviceId, user_id: userId };
const USR = JSON.stringify(usr);
const encryptedUSR = this.cipherHelper.encryptAsJson(USR, "USR", key, iv);
await this.set("USR", encryptedUSR);
this.cachedUSR = usr;
}
catch (error) {
this.errorHandler(error);
}
}
async validateUSR(USR) {
try {
const IID = this.cachedIID;
if (!IID)
return false;
const { key, iv } = this.cipherHelper.generateByInstallationID(IID);
const { USR: decryptedUSR } = this.cipherHelper.decryptAsJson(USR, key, iv);
return !!decryptedUSR;
}
catch (error) {
this.errorHandler(error);
return false;
}
}
// #endregion
// #region "Device Security Code"
async getDeviceSecurityCode() {
try {
const [IID, USR, DEK] = await Promise.all([
this.getIID(),
this.getUSR(),
this.getDEK(),
]);
if (!DEK || !USR || !IID) {
return "";
}
const { key, iv } = this.cipherHelper.generateByUserID(USR.user_id.toString());
const DUID = IID + "." + DEK;
// const encryptedJSON = this.cipherHelper.EncryptAsJson(
// DUID,
// "device_security_code",
// key,
// iv
// );
// const { device_security_code } = JSON.parse(encryptedJSON);
const device_security_code = this.cipherHelper.encrypt(DUID, key, iv);
return device_security_code;
}
catch (error) {
this.errorHandler(error);
return "";
}
}
// #endregion
// #region "Access Token"
async getAccessToken() {
try {
if (this.cachedAccessToken)
return this.cachedAccessToken;
const [IID, accessToken] = await Promise.all([
this.getIID(),
this.get("access_token"),
]);
if (!accessToken || !IID)
return "";
const { key, iv } = this.cipherHelper.generateByInstallationID(IID);
const { access_token } = this.cipherHelper.decryptAsJson(accessToken, key, iv);
this.cachedAccessToken = access_token;
return access_token;
}
catch (error) {
this.errorHandler(error);
return "";
}
}
async setAccessToken(accessToken, expires) {
try {
const IID = await this.getIID();
if (!IID)
return;
const { key, iv } = this.cipherHelper.generateByInstallationID(IID);
const encryptedAccessToken = this.cipherHelper.encryptAsJson(accessToken, "access_token", key, iv);
await this.set("access_token", encryptedAccessToken, expires);
this.cachedAccessToken = accessToken;
}
catch (error) {
this.errorHandler(error);
}
}
async validateAccessToken(accessToken) {
try {
const IID = this.cachedIID;
if (!IID)
return false;
const { key, iv } = this.cipherHelper.generateByInstallationID(IID);
const { access_token } = this.cipherHelper.decryptAsJson(accessToken, key, iv);
return !!access_token;
}
catch (error) {
this.errorHandler(error);
return false;
}
}
// #endregion
// #region "JWT Token"
async getJWTToken() {
try {
if (this.cachedJWTToken)
return this.cachedJWTToken;
const jwtToken = await this.get("jwt_token");
if (!jwtToken)
return "";
const { key, iv } = this.cipherHelper.generateByProgramID(this.programId);
const { jwt_token } = this.cipherHelper.decryptAsJson(jwtToken, key, iv);
this.cachedJWTToken = jwt_token;
return jwt_token;
}
catch (error) {
this.errorHandler(error);
return "";
}
}
async setJWTToken(jwtToken) {
try {
const { key, iv } = this.cipherHelper.generateByProgramID(this.programId);
const encryptedJWTToken = this.cipherHelper.encryptAsJson(jwtToken, "jwt_token", key, iv);
await this.set("jwt_token", encryptedJWTToken);
this.cachedJWTToken = jwtToken;
}
catch (error) {
this.errorHandler(error);
}
}
async validateJWTToken(jwtToken) {
try {
const { key, iv } = this.cipherHelper.generateByProgramID(this.programId);
const { jwt_token } = this.cipherHelper.decryptAsJson(jwtToken, key, iv);
return !!jwt_token;
}
catch (error) {
this.errorHandler(error);
return false;
}
}
// #endregion
// #region "Getters & Setters"
async get(key) {
const cookieName = this.buildCookieName(key);
try {
if (this.useCookieStore && this.Cookies) {
const cookie = await this.Cookies.get({ name: cookieName });
return cookie?.value || "";
}
else {
// Fallback to document.cookie
return this.getCookieFallback(cookieName);
}
}
catch (error) {
// If cookieStore fails, fallback to document.cookie
this.errorHandler(error);
return this.getCookieFallback(cookieName);
}
}
async set(key, value, expires) {
const defaultExpireDate = Date.now() + 1000 * 60 * 60 * 24 * 365;
const expireTime = expires ?? defaultExpireDate;
const isSecure = this.isSecureContext();
const cookieName = this.buildCookieName(key);
try {
if (this.useCookieStore && this.Cookies) {
await this.Cookies.set({
name: cookieName,
value: value,
domain: this.domain,
expires: expireTime,
path: "/",
sameSite: isSecure ? "none" : "lax",
secure: isSecure,
});
}
else {
// Fallback to document.cookie
this.setCookieFallback(cookieName, value, expireTime, this.domain);
}
}
catch (error) {
// If cookieStore fails, fallback to document.cookie
this.errorHandler(error);
this.setCookieFallback(cookieName, value, expireTime, this.domain);
}
}
async remove(key) {
const cookieName = this.buildCookieName(key);
try {
if (this.useCookieStore && this.Cookies) {
await this.Cookies.delete({
name: cookieName,
domain: this.domain,
path: "/",
});
}
else {
// Fallback to document.cookie
this.deleteCookieFallback(cookieName, this.domain);
}
}
catch (error) {
// If cookieStore fails, fallback to document.cookie
this.errorHandler(error);
this.deleteCookieFallback(cookieName, this.domain);
}
}
async setCredentialCookiesByTokenData(tokenData) {
try {
const { device_id, user_id, access_token, device_encryption_key, jwt_token, expires_in, } = tokenData;
await Promise.all([
this.setAccessToken(access_token, Date.now() + expires_in * 1000),
this.setJWTToken(jwt_token),
this.setUSR(device_id, user_id),
this.setDEK(device_encryption_key, {
user_id,
device_id,
}),
]);
}
catch (error) {
this.errorHandler(error);
}
}
async clearCredentialCookies() {
try {
this.cachedUSR = null;
this.cachedDEK = null;
this.cachedAccessToken = null;
this.cachedJWTToken = null;
await Promise.all([
this.remove("USR"),
this.remove("DEK"),
this.remove("access_token"),
this.remove("jwt_token"),
]);
}
catch (error) {
this.errorHandler(error);
}
}
// #endregion
// #region "Cookie Change Event Listener"
async cookieEventHandler(e, params) {
if (e.changed.length > 0) {
await this.onChangeHandler(e.changed, params);
}
if (e.deleted.length > 0) {
await this.onDeleteHandler(e.deleted, params);
}
}
async onChangeHandler(changed, params) {
const eventType = "change";
for (const changedCookie of changed) {
// const cookieName = this.parseCookieName(changedCookie.name);
const cookieName = changedCookie.name;
this.clearCacheForKey(cookieName);
switch (cookieName) {
case `IID-${this.programId}`: {
const isValidIID = await this.validateIID(changedCookie.value);
if (!isValidIID)
params.IID(eventType);
break;
}
case `USR-${this.programId}`: {
const isValidUSR = await this.validateUSR(changedCookie.value);
if (!isValidUSR)
params.USR(eventType);
break;
}
case `DEK-${this.programId}`: {
const isValidDEK = await this.validateDEK(changedCookie.value);
if (!isValidDEK)
params.DEK(eventType);
break;
}
case `access_token-${this.programId}`: {
const isValidAT = await this.validateAccessToken(changedCookie.value);
if (!isValidAT)
params.accessToken(eventType);
break;
}
case `jwt_token-${this.programId}`: {
const isValidJWT = await this.validateJWTToken(changedCookie.value);
if (!isValidJWT)
params.jwtToken(eventType);
break;
}
}
}
}
async onDeleteHandler(deleted, params) {
const eventType = "delete";
for (const deletedCookie of deleted) {
const cookieName = deletedCookie.name;
this.clearCacheForKey(cookieName);
switch (cookieName) {
case `IID-${this.programId}`:
params.IID(eventType);
break;
case `USR-${this.programId}`:
params.USR(eventType);
break;
case `DEK-${this.programId}`:
params.DEK(eventType);
break;
case `access_token-${this.programId}`:
params.accessToken(eventType);
break;
case `jwt_token-${this.programId}`:
params.jwtToken(eventType);
break;
}
}
}
addOnChangeEventListener(id, params) {
if (this.useCookieStore && this.Cookies) {
const handler = (e) => this.cookieEventHandler(e, params);
this.eventHandlers.set(id, handler);
this.Cookies.addEventListener("change", handler);
}
else {
console.warn("[CookiesHelper] cookieStore is unavailable — change events will not fire. Requires a secure context (HTTPS or localhost).");
}
}
removeOnChangeEventListener(id) {
if (this.useCookieStore && this.Cookies) {
const handler = this.eventHandlers.get(id);
if (handler) {
this.Cookies.removeEventListener("change", handler);
this.eventHandlers.delete(id);
}
}
}
// #endregion
// #region "Checkers"
async isAuthenticated() {
try {
const rawIID = await this.get("IID");
if (!rawIID)
return false;
const [IID, USR, DEK, accessToken, jwtToken] = await Promise.all([
this.getIID(),
this.getUSR(),
this.getDEK(),
this.getAccessToken(),
this.getJWTToken(),
]);
return !!IID && !!USR && !!DEK && !!accessToken && !!jwtToken;
}
catch (error) {
this.errorHandler(error);
return false;
}
}
// #endregion
// #region "Utils"
async getCredentialsCookies() {
try {
const [IID, USR, DEK, accessToken, jwtToken] = await Promise.all([
this.getIID(),
this.getUSR(),
this.getDEK(),
this.getAccessToken(),
this.getJWTToken(),
]);
const credentials = {
IID: IID || "",
USR: USR || { user_id: "0", device_id: "0" },
DEK: DEK || "",
access_token: accessToken || "",
jwt_token: jwtToken || "",
};
return credentials;
}
catch (error) {
this.errorHandler(error);
return {
IID: "",
USR: { user_id: "0", device_id: "0" },
DEK: "",
access_token: "",
jwt_token: "",
};
}
}
}
export { CookiesHelper };