make server lib use UMD, fixes to server test file and server code

This commit is contained in:
Tiago Rangel
2025-01-14 15:57:06 +00:00
parent 1bf731ccf2
commit ef80820b1c
3 changed files with 163 additions and 148 deletions
+161 -146
View File
@@ -14,179 +14,194 @@ const encodeBigInt = (num, base = 62) => {
return res || "0";
};
class Cap extends EventEmitter {
config = {
tokens_store_path: ".data/tokensList.json",
state: {
challengesList: {},
tokensList: {},
},
};
(function (define) {
define(function (require, exports, module) {
class Cap extends EventEmitter {
config;
#cleanupPromise = null;
#cleanupPromise = null;
constructor(configObj) {
super();
this.config = {
tokens_store_path: ".data/tokensList.json",
state: {
challengesList: {},
tokensList: {},
},
...configObj,
};
constructor(configObj) {
super();
this.config = { ...this.config, ...configObj };
this.#loadTokens().catch(() => {});
this.#loadTokens().catch(() => {});
process.on("beforeExit", () => this.cleanup());
process.on("beforeExit", () => this.cleanup());
["SIGINT", "SIGTERM", "SIGQUIT"].forEach((signal) => {
process.once(signal, () => {
this.cleanup()
.then(() => process.exit(0))
.catch(() => process.exit(1));
});
});
}
async #loadTokens() {
try {
const data = await fs.readFile(this.config.tokens_store_path, "utf-8");
this.config.state.tokensList = JSON.parse(data) || {};
this.#cleanExpiredTokens();
} catch (error) {
this.config.state.tokensList = {};
}
}
#cleanExpiredTokens() {
const now = Date.now();
let tokensChanged = false;
for (const k in this.config.state.challengesList) {
if (this.config.state.challengesList[k].expires < now) {
delete this.config.state.challengesList[k];
["SIGINT", "SIGTERM", "SIGQUIT"].forEach((signal) => {
process.once(signal, () => {
this.cleanup()
.then(() => process.exit(0))
.catch(() => process.exit(1));
});
});
}
}
for (const k in this.config.state.tokensList) {
if (this.config.state.tokensList[k] < now) {
delete this.config.state.tokensList[k];
tokensChanged = true;
async #loadTokens() {
try {
const data = await fs.readFile(
this.config.tokens_store_path,
"utf-8"
);
this.config.state.tokensList = JSON.parse(data) || {};
this.#cleanExpiredTokens();
} catch (error) {
this.config.state.tokensList = {};
}
}
}
return tokensChanged;
}
#cleanExpiredTokens() {
const now = Date.now();
let tokensChanged = false;
async cleanup() {
if (this.#cleanupPromise) return this.#cleanupPromise;
for (const k in this.config.state.challengesList) {
if (this.config.state.challengesList[k].expires < now) {
delete this.config.state.challengesList[k];
}
}
this.#cleanupPromise = (async () => {
const tokensChanged = this.#cleanExpiredTokens();
for (const k in this.config.state.tokensList) {
if (this.config.state.tokensList[k] < now) {
delete this.config.state.tokensList[k];
tokensChanged = true;
}
}
if (tokensChanged) {
await fs.writeFile(
this.config.tokens_store_path,
JSON.stringify(this.config.state.tokensList),
"utf8"
return tokensChanged;
}
async cleanup() {
if (this.#cleanupPromise) return this.#cleanupPromise;
this.#cleanupPromise = (async () => {
const tokensChanged = this.#cleanExpiredTokens();
if (tokensChanged) {
await fs.writeFile(
this.config.tokens_store_path,
JSON.stringify(this.config.state.tokensList),
"utf8"
);
}
})();
return this.#cleanupPromise;
}
createChallenge(conf) {
this.#cleanExpiredTokens();
const challenges = Array.from(
{ length: conf?.challengeCount || 18 },
() => [
crypto
.randomBytes(Math.ceil((conf?.challengeSize || 32) / 2))
.toString("hex")
.slice(0, conf?.challengeSize || 32),
crypto
.randomBytes(Math.ceil((conf?.challengeDifficulty || 4) / 2))
.toString("hex")
.slice(0, conf?.challengeDifficulty || 4),
]
);
const token = crypto.randomBytes(25).toString("hex");
const expires = Date.now() + (conf?.expiresMs || 600000);
if (conf?.store === false) {
return { challenge: challenges, expires };
}
this.config.state.challengesList[token] = {
challenge: challenges,
expires,
token,
};
return { challenge: challenges, token, expires };
}
})();
return this.#cleanupPromise;
}
async redeemChallenge({ token, solutions }) {
this.#cleanExpiredTokens();
createChallenge(conf) {
this.#cleanExpiredTokens();
const challengeData = this.config.state.challengesList[token];
if (!challengeData || challengeData.expires < Date.now()) {
delete this.config.state.challengesList[token];
return { success: false };
}
const challenges = Array.from(
{ length: conf?.challengeCount || 18 },
() => [
crypto
.randomBytes(Math.ceil((conf?.challengeSize || 32) / 2))
.toString("hex")
.slice(0, conf?.challengeSize || 32),
crypto
.randomBytes(Math.ceil((conf?.challengeDifficulty || 4) / 2))
.toString("hex")
.slice(0, conf?.challengeDifficulty || 4),
]
);
delete this.config.state.challengesList[token];
const token = crypto.randomBytes(25).toString("hex");
const expires = Date.now() + (conf?.expiresMs || 600000);
const isValid = challengeData.challenge.every(([salt, target]) => {
const solution = solutions.find(
([s, t]) => s === salt && t === target
);
return (
solution &&
crypto
.createHash("sha256")
.update(salt + solution[2])
.digest("hex")
.startsWith(target)
);
});
if (conf?.store === false) {
return { challenge: challenges, expires };
}
if (!isValid) return { success: false };
this.config.state.challengesList[token] = {
challenge: challenges,
expires,
token,
};
const vertoken = crypto.randomBytes(15).toString("hex");
const expires = Date.now() + 20 * 60 * 1000;
const hash = encodeBigInt(Bun.hash(vertoken));
const id = crypto.randomBytes(8).toString("hex");
return { challenge: challenges, token, expires };
}
async redeemChallenge({ token, solutions }) {
this.#cleanExpiredTokens();
const challengeData = this.config.state.challengesList[token];
if (!challengeData || challengeData.expires < Date.now()) {
delete this.config.state.challengesList[token];
return { success: false };
}
delete this.config.state.challengesList[token];
const isValid = challengeData.challenge.every(([salt, target]) => {
const solution = solutions.find(([s, t]) => s === salt && t === target);
return (
solution &&
crypto
.createHash("sha256")
.update(salt + solution[2])
.digest("hex")
.startsWith(target)
);
});
if (!isValid) return { success: false };
const vertoken = crypto.randomBytes(15).toString("hex");
const expires = Date.now() + 20 * 60 * 1000;
const hash = encodeBigInt(Bun.hash(vertoken));
const id = crypto.randomBytes(8).toString("hex");
this.config.state.tokensList[`${id}:${hash}`] = expires;
await fs.writeFile(
this.config.tokens_store_path,
JSON.stringify(this.config.state.tokensList),
"utf8"
);
return { success: true, token: `${id}:${vertoken}`, expires };
}
async validateToken(token, conf) {
this.#cleanExpiredTokens();
const [id, vertoken] = token.split(":");
const hash = encodeBigInt(Bun.hash(vertoken));
const key = `${id}:${hash}`;
if (this.config.state.tokensList[key]) {
if (conf.keepToken) {
delete this.config.state.tokensList[key];
this.config.state.tokensList[`${id}:${hash}`] = expires;
await fs.writeFile(
this.config.tokens_store_path,
JSON.stringify(this.config.state.tokensList),
"utf8"
);
return { success: true, token: `${id}:${vertoken}`, expires };
}
return { success: true };
async validateToken(token, conf) {
this.#cleanExpiredTokens();
const [id, vertoken] = token.split(":");
const hash = encodeBigInt(Bun.hash(vertoken));
const key = `${id}:${hash}`;
if (this.config.state.tokensList[key]) {
if (conf?.keepToken) {
delete this.config.state.tokensList[key];
await fs.writeFile(
this.config.tokens_store_path,
JSON.stringify(this.config.state.tokensList),
"utf8"
);
}
return { success: true };
}
return { success: false };
}
}
return { success: false };
}
}
module.exports = Cap;
return Cap;
});
})(
typeof module === "object" && module.exports && typeof define !== "function"
? function (factory) {
module.exports = factory(require, exports, module);
}
: define
);
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@cap.js/server",
"version": "1.0.1",
"version": "1.0.2",
"description": "Cap server-side helper",
"keywords": [
"captcha",
+1 -1
View File
@@ -42,7 +42,7 @@ app.post('/api/validate', async (req, res) => {
if (!token) {
return res.status(400).json({ success: false });
}
res.json(await cap.validateToken({ token }));
res.json(await cap.validateToken(token));
});
async function test() {