react-native-mosquito-transport
Version:
React native javascript sdk for mosquito-transport (https://github.com/brainbehindx/mosquito-transport)
265 lines (224 loc) • 10.5 kB
JavaScript
import { doSignOut, revokeAuthIntance } from "./index.js";
import EngineApi from "../../helpers/engine_api";
import { AuthTokenListener, TokenRefreshListener } from "../../helpers/listeners";
import { decodeBinary, deserializeE2E } from "../../helpers/peripherals";
import { awaitReachableServer, awaitStore, buildFetchInterface, buildFetchResult, getReachableServer, updateCacheStore } from "../../helpers/utils";
import { CacheStore, Scoped } from "../../helpers/variables";
import { simplifyError } from "simplify-error";
import { Validator } from "guard-object";
import { basicClone } from "../../helpers/basic_clone";
import NativeMosquitoTransport from "../../NativeMosquitoTransport.js";
export const listenToken = (callback, projectUrl) =>
AuthTokenListener.listenToPersist(projectUrl, (t, n) => {
if (t === undefined) return;
callback?.(t || null, n);
});
export const injectFreshToken = async (config, { token, refreshToken }) => {
const { projectUrl } = config;
CacheStore.AuthStore[projectUrl] = { token, refreshToken };
Scoped.AuthJWTToken[projectUrl] = token;
const isEmulated = projectUrl in CacheStore.EmulatedAuth;
if (isEmulated) delete CacheStore.EmulatedAuth[projectUrl];
await updateTokenTimestamp(projectUrl, token);
updateCacheStore(['AuthStore', isEmulated ? 'EmulatedAuth' : ''].filter(v => v));
triggerAuthToken(projectUrl);
initTokenRefresher({ config });
};
export const injectEmulatedAuth = async (config, emulatedURL) => {
if (!Scoped.IsStoreReady) await awaitStore();
if (typeof emulatedURL !== 'string' || (!Validator.HTTPS(emulatedURL) && !Validator.HTTP(emulatedURL)))
throw `Expected "projectUrl" to be valid https or http link but got "${emulatedURL}"`;
const { projectUrl } = config;
const { token } = CacheStore.AuthStore[emulatedURL] || {};
const depended = Object.entries(CacheStore.EmulatedAuth).find(([_, v]) => projectUrl === v);
if (emulatedURL === projectUrl) throw `auth instance for ${emulatedURL} cannot emulate itself`;
if (depended) throw `Chain Emulation Error: this auth instance (${projectUrl}) cannot be emulated as other auth instance (${depended[0]}) is already emulating it`;
const thisAuthStore = basicClone(CacheStore.AuthStore[projectUrl]);
revokeAuthIntance(config, thisAuthStore);
CacheStore.AuthStore[projectUrl] = basicClone(CacheStore.AuthStore[emulatedURL]);
Scoped.AuthJWTToken[projectUrl] = token;
CacheStore.EmulatedAuth[projectUrl] = emulatedURL;
updateCacheStore(['AuthStore', 'EmulatedAuth']);
triggerAuthToken(projectUrl);
initTokenRefresher({ config });
};
export const parseToken = (token) => JSON.parse(decodeBinary(token.split('.')[1]));
export const triggerAuthToken = async (projectUrl, isInit) => {
if (!Scoped.IsStoreReady) await awaitStore();
AuthTokenListener.dispatchPersist(projectUrl, CacheStore.AuthStore[projectUrl]?.token || null, isInit);
};
export const awaitRefreshToken = (projectUrl) =>
new Promise(async resolve => {
try {
if (await initTokenRefresher({ justCheck: true, config: Scoped.InitializedProject[projectUrl] })) {
resolve();
} else throw null;
} catch (_) {
const l = TokenRefreshListener.listenToPersist(projectUrl, v => {
if (v) {
l();
resolve();
}
});
}
});
export const ensureActiveToken = async (projectUrl) => {
if (await getReachableServer(projectUrl)) {
await awaitRefreshToken(projectUrl);
} else {
const emulatedURL = CacheStore.EmulatedAuth[projectUrl];
const { token } = CacheStore.AuthStore[emulatedURL || projectUrl] || {};
if (token && await hasTokenExpire(emulatedURL || projectUrl)) {
throw 'unable to refreshed expired token because of unreachable internet connection';
}
}
}
export const listenTokenReady = (callback, projectUrl) => {
let lastToken;
let resetDelayTimer;
let postCaller;
const listener = TokenRefreshListener.listenToPersist(projectUrl, (...args) => {
const newTruthy = !!Scoped.AuthJWTToken[projectUrl];
if (resetDelayTimer !== undefined) {
if (newTruthy) {
clearTimeout(resetDelayTimer);
resetDelayTimer = undefined;
postCaller = undefined;
callback?.(...args);
} else {
postCaller = () => callback?.(...args);
}
} else if (typeof lastToken === 'boolean' && !newTruthy && lastToken) {
callback?.(false);
resetDelayTimer = setTimeout(() => {
resetDelayTimer = undefined;
if (postCaller) {
postCaller();
postCaller = undefined;
} else callback?.(...args);
}, 3000);
} else callback?.(...args);
lastToken = newTruthy;
});
return () => {
postCaller = undefined;
listener?.();
clearTimeout(resetDelayTimer);
}
}
export const initTokenRefresher = async ({ config, forceRefresh, justCheck }) => {
const { projectUrl, maxRetries } = config;
if (!Scoped.IsStoreReady) await awaitStore();
const { token } = CacheStore.AuthStore[projectUrl] || {};
const emulatedURL = CacheStore.EmulatedAuth[projectUrl];
if (!justCheck) clearInterval(Scoped.TokenRefreshTimer[projectUrl]);
if (emulatedURL) return;
const notifyAuthReady = (value) => {
if (justCheck) return;
TokenRefreshListener.dispatchPersist(projectUrl, value);
getEmulatedLinks(projectUrl).forEach(v => {
TokenRefreshListener.dispatchPersist(v, value);
});
}
if (token) {
const rizz = () => {
let runningProcess = Scoped.TokenRefreshProcess[projectUrl];
if (!runningProcess) {
runningProcess = refreshToken(config, maxRetries, forceRefresh);
Scoped.TokenRefreshProcess[projectUrl] = runningProcess;
Scoped.TokenRefreshProcess[projectUrl].finally(() => {
delete Scoped.TokenRefreshProcess[projectUrl];
});
}
return runningProcess;
}
if (await hasTokenExpire(projectUrl) || forceRefresh) {
notifyAuthReady();
return rizz();
} else {
notifyAuthReady(true);
if (justCheck) {
return true;
} else {
let lastIte = 0;
clearInterval(Scoped.TokenRefreshTimer[projectUrl]);
Scoped.TokenRefreshTimer[projectUrl] = setInterval(async () => {
const iteRef = ++lastIte;
if (!(await hasTokenExpire(projectUrl)) || iteRef !== lastIte) return;
clearInterval(Scoped.TokenRefreshTimer[projectUrl]);
notifyAuthReady();
rizz();
}, 7000);
}
}
} else {
notifyAuthReady(true);
if (justCheck) return true;
}
};
const hasTokenExpire = async (projectUrl) => {
const timestamp = Scoped.TokenTimestamping[projectUrl];
if (!timestamp) return true;
const uptime = await NativeMosquitoTransport.getSystemUptime();
return timestamp.ttl <= (uptime - timestamp.uptime);
}
const updateTokenTimestamp = async (projectUrl, token) => {
const { exp, iat } = parseToken(token);
Scoped.TokenTimestamping[projectUrl] = {
ttl: ((exp * 1000) - (iat * 1000)) - 60_000,
uptime: await NativeMosquitoTransport.getSystemUptime()
};
}
export const getEmulatedLinks = (projectUrl) =>
Object.entries(CacheStore.EmulatedAuth)
.filter(([_, v]) => v === projectUrl)
.map(v => v[0]);
const refreshToken = (builder, remainRetries = 3) =>
new Promise(async (resolve, reject) => {
const { projectUrl, serverE2E_PublicKey, uglify, extraHeaders } = builder;
try {
const { token, refreshToken: r_token } = CacheStore.AuthStore[projectUrl];
const [reqBuilder, [privateKey]] = await buildFetchInterface({
body: { token, r_token },
uglify,
serverE2E_PublicKey,
extraHeaders
});
const data = await buildFetchResult(await fetch(EngineApi._refreshAuthToken(projectUrl, uglify), reqBuilder), uglify);
const f = uglify ? await deserializeE2E(data, serverE2E_PublicKey, privateKey) : data;
if (!CacheStore.AuthStore[projectUrl]) {
reject(simplifyError('token_not_mounted', 'No refresh token was mounted or has been recently removed').simpleError);
return;
}
CacheStore.AuthStore[projectUrl].token = f.result.token;
Scoped.AuthJWTToken[projectUrl] = f.result.token;
await updateTokenTimestamp(projectUrl, f.result.token);
resolve(f.result.token);
const isInit = !Scoped.InitiatedForcedToken[projectUrl];
triggerAuthToken(projectUrl, isInit);
if (isInit) Scoped.InitiatedForcedToken[projectUrl] = true;
getEmulatedLinks(projectUrl).forEach(v => {
CacheStore.AuthStore[v] = basicClone(CacheStore.AuthStore[projectUrl]);
Scoped.AuthJWTToken[v] = f.result.token;
triggerAuthToken(v, isInit);
if (isInit) Scoped.InitiatedForcedToken[v] = true;
});
updateCacheStore(['AuthStore']);
initTokenRefresher({ config: builder });
} catch (e) {
if (e.simpleError) {
console.error(`refreshToken error: ${e.simpleError?.message}`);
doSignOut({ ...builder });
reject(e.simpleError);
} else if (remainRetries <= 0) {
reject(
simplifyError('retry_limit_reached', 'The retry limit has been reach and execution prematurely stopped').simpleError
);
console.error(`refreshToken retry limit exceeded err:`, e);
} else {
awaitReachableServer(projectUrl, true).then(() => {
refreshToken(builder, remainRetries - 1).then(resolve, reject);
});
}
}
});