server: add types
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "@cap.js/server",
|
||||
"dependencies": {
|
||||
"@types/node": "^22.14.1",
|
||||
"typescript": "^5.8.3",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@types/node": ["@types/node@22.14.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw=="],
|
||||
|
||||
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
|
||||
|
||||
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||
}
|
||||
}
|
||||
Vendored
+149
@@ -0,0 +1,149 @@
|
||||
export = Cap;
|
||||
/**
|
||||
* Main Cap class
|
||||
* @extends EventEmitter
|
||||
*/
|
||||
declare class Cap extends EventEmitter<[never]> {
|
||||
/**
|
||||
* Creates a new Cap instance
|
||||
* @param {Partial<CapConfig>} [configObj] - Configuration object
|
||||
*/
|
||||
constructor(configObj?: Partial<CapConfig>);
|
||||
/** @type {Promise<void>|null} */
|
||||
_cleanupPromise: Promise<void> | null;
|
||||
/** @type {Required<CapConfig>} */
|
||||
config: Required<CapConfig>;
|
||||
/**
|
||||
* Generates a new challenge
|
||||
* @param {ChallengeConfig} [conf] - Challenge configuration
|
||||
* @returns {{ challenge: Array<ChallengeTuple>, token?: string, expires: number }} Challenge data
|
||||
*/
|
||||
createChallenge(conf?: ChallengeConfig): {
|
||||
challenge: Array<ChallengeTuple>;
|
||||
token?: string;
|
||||
expires: number;
|
||||
};
|
||||
/**
|
||||
* Redeems a challenge solution in exchange for a token
|
||||
* @param {Solution} param0 - Challenge solution data
|
||||
* @returns {Promise<{success: boolean, message?: string, token?: string, expires?: number}>}
|
||||
*/
|
||||
redeemChallenge({ token, solutions }: Solution): Promise<{
|
||||
success: boolean;
|
||||
message?: string;
|
||||
token?: string;
|
||||
expires?: number;
|
||||
}>;
|
||||
/**
|
||||
* Validates a token
|
||||
* @param {string} token - The token to validate
|
||||
* @param {TokenConfig} [conf] - Validation configuration
|
||||
* @returns {Promise<{success: boolean}>}
|
||||
*/
|
||||
validateToken(token: string, conf?: TokenConfig): Promise<{
|
||||
success: boolean;
|
||||
}>;
|
||||
/**
|
||||
* Loads tokens from the storage file
|
||||
* @private
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
private _loadTokens;
|
||||
/**
|
||||
* Removes expired tokens and challenges from memory
|
||||
* @private
|
||||
* @returns {boolean} - True if any tokens were changed/removed
|
||||
*/
|
||||
private _cleanExpiredTokens;
|
||||
/**
|
||||
* Waits for the tokens list to be initialized
|
||||
* @private
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
private _waitForTokensList;
|
||||
/**
|
||||
* Cleans up expired tokens and syncs state
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
cleanup(): Promise<void>;
|
||||
}
|
||||
declare namespace Cap {
|
||||
export { Crypto, FsPromises, PathLike, ChallengeTuple, ChallengeData, ChallengeState, ChallengeConfig, TokenConfig, Solution, CapConfig };
|
||||
}
|
||||
import { EventEmitter } from "events";
|
||||
type Crypto = typeof import("node:crypto");
|
||||
type FsPromises = typeof import("node:fs/promises");
|
||||
type PathLike = import("fs").PathLike;
|
||||
type ChallengeTuple = [string, string];
|
||||
type ChallengeData = {
|
||||
/**
|
||||
* - Array of [salt, target] tuples
|
||||
*/
|
||||
challenge: Array<ChallengeTuple>;
|
||||
/**
|
||||
* - Expiration timestamp
|
||||
*/
|
||||
expires: number;
|
||||
/**
|
||||
* - Challenge token
|
||||
*/
|
||||
token: string;
|
||||
};
|
||||
type ChallengeState = {
|
||||
/**
|
||||
* - Map of challenge tokens to challenge data
|
||||
*/
|
||||
challengesList: Record<string, ChallengeData>;
|
||||
/**
|
||||
* - Map of token hashes to expiration timestamps
|
||||
*/
|
||||
tokensList: Record<string, number>;
|
||||
};
|
||||
type ChallengeConfig = {
|
||||
/**
|
||||
* - Number of challenges to generate
|
||||
*/
|
||||
challengeCount?: number | undefined;
|
||||
/**
|
||||
* - Size of each challenge in bytes
|
||||
*/
|
||||
challengeSize?: number | undefined;
|
||||
/**
|
||||
* - Difficulty level of the challenge
|
||||
*/
|
||||
challengeDifficulty?: number | undefined;
|
||||
/**
|
||||
* - Time in milliseconds until the challenge expires
|
||||
*/
|
||||
expiresMs?: number | undefined;
|
||||
/**
|
||||
* - Whether to store the challenge in memory
|
||||
*/
|
||||
store?: boolean | undefined;
|
||||
};
|
||||
type TokenConfig = {
|
||||
/**
|
||||
* - Whether to keep the token after validation
|
||||
*/
|
||||
keepToken?: boolean | undefined;
|
||||
};
|
||||
type Solution = {
|
||||
/**
|
||||
* - The challenge token
|
||||
*/
|
||||
token: string;
|
||||
/**
|
||||
* - Array of [salt, target, solution] tuples
|
||||
*/
|
||||
solutions: Array<[string, string, string]>;
|
||||
};
|
||||
type CapConfig = {
|
||||
/**
|
||||
* - Path to store the tokens file
|
||||
*/
|
||||
tokens_store_path: string;
|
||||
/**
|
||||
* - State configuration
|
||||
*/
|
||||
state: ChallengeState;
|
||||
};
|
||||
+277
-183
@@ -1,228 +1,322 @@
|
||||
// @ts-check
|
||||
/// <reference lib="dom" />
|
||||
/// <reference types="node" />
|
||||
|
||||
/**
|
||||
* @typedef {import('node:crypto')} Crypto
|
||||
* @typedef {import('node:fs/promises')} FsPromises
|
||||
* @typedef {import('fs').PathLike} PathLike
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {[string, string]} ChallengeTuple
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} ChallengeData
|
||||
* @property {Array<ChallengeTuple>} challenge - Array of [salt, target] tuples
|
||||
* @property {number} expires - Expiration timestamp
|
||||
* @property {string} token - Challenge token
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} ChallengeState
|
||||
* @property {Record<string, ChallengeData>} challengesList - Map of challenge tokens to challenge data
|
||||
* @property {Record<string, number>} tokensList - Map of token hashes to expiration timestamps
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} ChallengeConfig
|
||||
* @property {number} [challengeCount=18] - Number of challenges to generate
|
||||
* @property {number} [challengeSize=32] - Size of each challenge in bytes
|
||||
* @property {number} [challengeDifficulty=4] - Difficulty level of the challenge
|
||||
* @property {number} [expiresMs=600000] - Time in milliseconds until the challenge expires
|
||||
* @property {boolean} [store=true] - Whether to store the challenge in memory
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} TokenConfig
|
||||
* @property {boolean} [keepToken] - Whether to keep the token after validation
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} Solution
|
||||
* @property {string} token - The challenge token
|
||||
* @property {Array<[string, string, string]>} solutions - Array of [salt, target, solution] tuples
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} CapConfig
|
||||
* @property {string} tokens_store_path - Path to store the tokens file
|
||||
* @property {ChallengeState} state - State configuration
|
||||
*/
|
||||
|
||||
/** @type {typeof import('node:crypto')} */
|
||||
const crypto = require("crypto");
|
||||
/** @type {typeof import('node:fs/promises')} */
|
||||
const fs = require("fs/promises");
|
||||
const { EventEmitter } = require("events");
|
||||
|
||||
const DEFAULT_TOKENS_STORE = ".data/tokensList.json";
|
||||
|
||||
(function (define) {
|
||||
define(function (require, exports, module) {
|
||||
class Cap extends EventEmitter {
|
||||
constructor(configObj) {
|
||||
super();
|
||||
this._cleanupPromise = null;
|
||||
this.config = {
|
||||
tokens_store_path: DEFAULT_TOKENS_STORE,
|
||||
state: {
|
||||
challengesList: {},
|
||||
tokensList: {},
|
||||
},
|
||||
...configObj,
|
||||
};
|
||||
/**
|
||||
* Main Cap class
|
||||
* @extends EventEmitter
|
||||
*/
|
||||
class Cap extends EventEmitter {
|
||||
/** @type {Promise<void>|null} */
|
||||
_cleanupPromise;
|
||||
|
||||
this._loadTokens().catch(() => { });
|
||||
/** @type {Required<CapConfig>} */
|
||||
config;
|
||||
|
||||
process.on("beforeExit", () => this.cleanup());
|
||||
/**
|
||||
* Creates a new Cap instance
|
||||
* @param {Partial<CapConfig>} [configObj] - Configuration object
|
||||
*/
|
||||
constructor(configObj) {
|
||||
super();
|
||||
this._cleanupPromise = null;
|
||||
/** @type {Required<CapConfig>} */
|
||||
this.config = {
|
||||
tokens_store_path: DEFAULT_TOKENS_STORE,
|
||||
state: {
|
||||
challengesList: {},
|
||||
tokensList: {},
|
||||
},
|
||||
...configObj,
|
||||
};
|
||||
|
||||
["SIGINT", "SIGTERM", "SIGQUIT"].forEach((signal) => {
|
||||
process.once(signal, () => {
|
||||
this.cleanup()
|
||||
.then(() => process.exit(0))
|
||||
.catch(() => process.exit(1));
|
||||
});
|
||||
});
|
||||
}
|
||||
this._loadTokens().catch(() => {});
|
||||
|
||||
async _loadTokens() {
|
||||
try {
|
||||
const dirPath = this.config.tokens_store_path.split('/').slice(0, -1).join('/');
|
||||
if (dirPath) {
|
||||
await fs.mkdir(dirPath, { recursive: true });
|
||||
}
|
||||
process.on("beforeExit", () => this.cleanup());
|
||||
|
||||
try {
|
||||
await fs.access(this.config.tokens_store_path);
|
||||
const data = await fs.readFile(
|
||||
this.config.tokens_store_path,
|
||||
"utf-8"
|
||||
);
|
||||
this.config.state.tokensList = JSON.parse(data) || {};
|
||||
this._cleanExpiredTokens();
|
||||
} catch {
|
||||
console.log(
|
||||
`[cap] Tokens file not found, creating a new empty one`
|
||||
);
|
||||
await fs.writeFile(this.config.tokens_store_path, "{}", "utf-8");
|
||||
this.config.state.tokensList = {};
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(
|
||||
`[cap] Couldn't load or write tokens file, using empty state`
|
||||
);
|
||||
this.config.state.tokensList = {};
|
||||
}
|
||||
}
|
||||
["SIGINT", "SIGTERM", "SIGQUIT"].forEach((signal) => {
|
||||
process.once(signal, () => {
|
||||
this.cleanup()
|
||||
.then(() => process.exit(0))
|
||||
.catch(() => process.exit(1));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_cleanExpiredTokens() {
|
||||
const now = Date.now();
|
||||
let tokensChanged = false;
|
||||
/**
|
||||
* Generates a new challenge
|
||||
* @param {ChallengeConfig} [conf] - Challenge configuration
|
||||
* @returns {{ challenge: Array<ChallengeTuple>, token?: string, expires: number }} Challenge data
|
||||
*/
|
||||
createChallenge(conf) {
|
||||
this._cleanExpiredTokens();
|
||||
|
||||
for (const k in this.config.state.challengesList) {
|
||||
if (this.config.state.challengesList[k].expires < now) {
|
||||
delete this.config.state.challengesList[k];
|
||||
}
|
||||
}
|
||||
/** @type {Array<ChallengeTuple>} */
|
||||
const challenges = Array.from(
|
||||
{ length: (conf && conf.challengeCount) || 18 },
|
||||
() =>
|
||||
/** @type {ChallengeTuple} */ ([
|
||||
crypto
|
||||
.randomBytes(Math.ceil(((conf && conf.challengeSize) || 32) / 2))
|
||||
.toString("hex")
|
||||
.slice(0, (conf && conf.challengeSize) || 32),
|
||||
crypto
|
||||
.randomBytes(
|
||||
Math.ceil(((conf && conf.challengeDifficulty) || 4) / 2)
|
||||
)
|
||||
.toString("hex")
|
||||
.slice(0, (conf && conf.challengeDifficulty) || 4),
|
||||
])
|
||||
);
|
||||
|
||||
for (const k in this.config.state.tokensList) {
|
||||
if (this.config.state.tokensList[k] < now) {
|
||||
delete this.config.state.tokensList[k];
|
||||
tokensChanged = true;
|
||||
}
|
||||
}
|
||||
const token = crypto.randomBytes(25).toString("hex");
|
||||
const expires = Date.now() + ((conf && conf.expiresMs) || 600000);
|
||||
|
||||
return tokensChanged;
|
||||
}
|
||||
if (conf && conf.store === false) {
|
||||
return { challenge: challenges, expires };
|
||||
}
|
||||
|
||||
_waitForTokensList() {
|
||||
return new Promise((resolve) => {
|
||||
const l = () => {
|
||||
if (this.config.state.tokensList) {
|
||||
return resolve();
|
||||
}
|
||||
setTimeout(l, 10);
|
||||
};
|
||||
l();
|
||||
});
|
||||
}
|
||||
this.config.state.challengesList[token] = {
|
||||
challenge: challenges,
|
||||
expires,
|
||||
token,
|
||||
};
|
||||
|
||||
async cleanup() {
|
||||
if (this._cleanupPromise) return this._cleanupPromise;
|
||||
return { challenge: challenges, token, expires };
|
||||
}
|
||||
|
||||
this._cleanupPromise = (async () => {
|
||||
const tokensChanged = this._cleanExpiredTokens();
|
||||
/**
|
||||
* Redeems a challenge solution in exchange for a token
|
||||
* @param {Solution} param0 - Challenge solution data
|
||||
* @returns {Promise<{success: boolean, message?: string, token?: string, expires?: number}>}
|
||||
*/
|
||||
async redeemChallenge({ token, solutions }) {
|
||||
this._cleanExpiredTokens();
|
||||
|
||||
if (tokensChanged) {
|
||||
await fs.writeFile(
|
||||
this.config.tokens_store_path,
|
||||
JSON.stringify(this.config.state.tokensList),
|
||||
"utf8"
|
||||
);
|
||||
}
|
||||
})();
|
||||
const challengeData = this.config.state.challengesList[token];
|
||||
if (!challengeData || challengeData.expires < Date.now()) {
|
||||
delete this.config.state.challengesList[token];
|
||||
return { success: false, message: "Challenge expired" };
|
||||
}
|
||||
|
||||
return this._cleanupPromise;
|
||||
}
|
||||
delete this.config.state.challengesList[token];
|
||||
|
||||
createChallenge(conf) {
|
||||
this._cleanExpiredTokens();
|
||||
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)
|
||||
);
|
||||
});
|
||||
|
||||
const challenges = Array.from(
|
||||
{ length: (conf && conf.challengeCount) || 18 },
|
||||
() => [
|
||||
crypto
|
||||
.randomBytes(Math.ceil(((conf && conf.challengeSize) || 32) / 2))
|
||||
.toString("hex")
|
||||
.slice(0, (conf && conf.challengeSize) || 32),
|
||||
crypto
|
||||
.randomBytes(
|
||||
Math.ceil(((conf && conf.challengeDifficulty) || 4) / 2)
|
||||
)
|
||||
.toString("hex")
|
||||
.slice(0, (conf && conf.challengeDifficulty) || 4),
|
||||
]
|
||||
);
|
||||
if (!isValid) return { success: false, message: "Invalid solution" };
|
||||
|
||||
const token = crypto.randomBytes(25).toString("hex");
|
||||
const expires = Date.now() + ((conf && conf.expiresMs) || 600000);
|
||||
const vertoken = crypto.randomBytes(15).toString("hex");
|
||||
const expires = Date.now() + 20 * 60 * 1000;
|
||||
const hash = crypto.createHash("sha256").update(vertoken).digest("hex");
|
||||
const id = crypto.randomBytes(8).toString("hex");
|
||||
|
||||
if (conf && conf.store === false) {
|
||||
return { challenge: challenges, expires };
|
||||
}
|
||||
this.config.state.tokensList[`${id}:${hash}`] = expires;
|
||||
|
||||
this.config.state.challengesList[token] = {
|
||||
challenge: challenges,
|
||||
expires,
|
||||
token,
|
||||
};
|
||||
await fs.writeFile(
|
||||
this.config.tokens_store_path,
|
||||
JSON.stringify(this.config.state.tokensList),
|
||||
"utf8"
|
||||
);
|
||||
|
||||
return { challenge: challenges, token, expires };
|
||||
}
|
||||
return { success: true, token: `${id}:${vertoken}`, expires };
|
||||
}
|
||||
|
||||
async redeemChallenge({ token, solutions }) {
|
||||
this._cleanExpiredTokens();
|
||||
/**
|
||||
* Validates a token
|
||||
* @param {string} token - The token to validate
|
||||
* @param {TokenConfig} [conf] - Validation configuration
|
||||
* @returns {Promise<{success: boolean}>}
|
||||
*/
|
||||
async validateToken(token, 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, message: "Challenge expired" };
|
||||
}
|
||||
const [id, vertoken] = token.split(":");
|
||||
const hash = crypto.createHash("sha256").update(vertoken).digest("hex");
|
||||
const key = `${id}:${hash}`;
|
||||
|
||||
delete this.config.state.challengesList[token];
|
||||
await this._waitForTokensList();
|
||||
|
||||
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, message: "Invalid solution" };
|
||||
|
||||
const vertoken = crypto.randomBytes(15).toString("hex");
|
||||
const expires = Date.now() + 20 * 60 * 1000;
|
||||
const hash = crypto.createHash("sha256").update(vertoken).digest("hex");
|
||||
const id = crypto.randomBytes(8).toString("hex");
|
||||
|
||||
this.config.state.tokensList[`${id}:${hash}`] = expires;
|
||||
if (this.config.state.tokensList[key]) {
|
||||
if (conf && 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, token: `${id}:${vertoken}`, expires };
|
||||
}
|
||||
|
||||
async validateToken(token, conf) {
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads tokens from the storage file
|
||||
* @private
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async _loadTokens() {
|
||||
try {
|
||||
const dirPath = this.config.tokens_store_path
|
||||
.split("/")
|
||||
.slice(0, -1)
|
||||
.join("/");
|
||||
if (dirPath) {
|
||||
await fs.mkdir(dirPath, { recursive: true });
|
||||
}
|
||||
|
||||
try {
|
||||
await fs.access(this.config.tokens_store_path);
|
||||
const data = await fs.readFile(this.config.tokens_store_path, "utf-8");
|
||||
this.config.state.tokensList = JSON.parse(data) || {};
|
||||
this._cleanExpiredTokens();
|
||||
} catch {
|
||||
console.log(`[cap] Tokens file not found, creating a new empty one`);
|
||||
await fs.writeFile(this.config.tokens_store_path, "{}", "utf-8");
|
||||
this.config.state.tokensList = {};
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(
|
||||
`[cap] Couldn't load or write tokens file, using empty state`
|
||||
);
|
||||
this.config.state.tokensList = {};
|
||||
}
|
||||
}
|
||||
|
||||
const [id, vertoken] = token.split(":");
|
||||
const hash = crypto.createHash("sha256").update(vertoken).digest("hex");
|
||||
const key = `${id}:${hash}`;
|
||||
/**
|
||||
* Removes expired tokens and challenges from memory
|
||||
* @private
|
||||
* @returns {boolean} - True if any tokens were changed/removed
|
||||
*/
|
||||
_cleanExpiredTokens() {
|
||||
const now = Date.now();
|
||||
let tokensChanged = false;
|
||||
|
||||
await this._waitForTokensList();
|
||||
|
||||
if (this.config.state.tokensList[key]) {
|
||||
if (conf && 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 };
|
||||
for (const k in this.config.state.challengesList) {
|
||||
if (this.config.state.challengesList[k].expires < now) {
|
||||
delete this.config.state.challengesList[k];
|
||||
}
|
||||
}
|
||||
|
||||
return Cap;
|
||||
});
|
||||
})(
|
||||
typeof module === "object" && module.exports && typeof define !== "function"
|
||||
? function (factory) {
|
||||
module.exports = factory(require, exports, module);
|
||||
for (const k in this.config.state.tokensList) {
|
||||
if (this.config.state.tokensList[k] < now) {
|
||||
delete this.config.state.tokensList[k];
|
||||
tokensChanged = true;
|
||||
}
|
||||
}
|
||||
: define
|
||||
);
|
||||
|
||||
return tokensChanged;
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for the tokens list to be initialized
|
||||
* @private
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
_waitForTokensList() {
|
||||
return new Promise((resolve) => {
|
||||
const l = () => {
|
||||
if (this.config.state.tokensList) {
|
||||
return resolve();
|
||||
}
|
||||
setTimeout(l, 10);
|
||||
};
|
||||
l();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up expired tokens and syncs state
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {typeof Cap} */
|
||||
module.exports = Cap;
|
||||
|
||||
+8
-3
@@ -1,12 +1,13 @@
|
||||
{
|
||||
"name": "@cap.js/server",
|
||||
"version": "1.0.8",
|
||||
"author": "Tiago Rangel",
|
||||
"version": "1.0.9",
|
||||
"author": "Tiago",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tiagorangel1/cap.git"
|
||||
},
|
||||
"main": "index.js",
|
||||
"types": "index.d.ts",
|
||||
"bugs": {
|
||||
"url": "https://github.com/tiagorangel1/cap/issues"
|
||||
},
|
||||
@@ -227,5 +228,9 @@
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"npm:publish": "sudo npm publish --access public"
|
||||
},
|
||||
"type": "commonjs"
|
||||
"type": "commonjs",
|
||||
"dependencies": {
|
||||
"@types/node": "^22.14.1",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"checkJs": true,
|
||||
"allowJs": true,
|
||||
"declaration": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"strict": true,
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"types": ["node"],
|
||||
"outDir": "."
|
||||
},
|
||||
"include": ["./index.js"],
|
||||
"exclude": []
|
||||
}
|
||||
@@ -77,8 +77,8 @@
|
||||
padding: 6px 10px;
|
||||
border-radius: 6px;
|
||||
border: none;
|
||||
color: black;
|
||||
background: white;
|
||||
color: white;
|
||||
background: #0076D8;
|
||||
background-origin: border-box;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
@@ -90,9 +90,9 @@
|
||||
}
|
||||
|
||||
button.btn:focus {
|
||||
box-shadow: inset 0px 0.8px 0px -0.25px rgba(255, 255, 255, 0.2),
|
||||
box-shadow: inset 0px 0.8px 0px -0.25px rgba(0, 118, 216, 0.2),
|
||||
0px 0.5px 1.5px rgba(54, 122, 246, 0.25),
|
||||
0px 0px 0px 3.5px rgba(255, 255, 255, 0.5);
|
||||
0px 0px 0px 3.5px rgba(0, 118, 216, 0.5);
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
@@ -188,13 +188,14 @@
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
padding: 10px 14px;
|
||||
background-color: #2a2a31;
|
||||
border: 1px solid rgba(255, 255, 255, 0.17);
|
||||
background-color: transparent;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.key-creation-form input:focus {
|
||||
outline: none;
|
||||
box-shadow: 0px 0px 0px 2px rgba(255, 255, 255, 0.5);
|
||||
box-shadow: 0px 0px 0px 2px rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.key-creation-form input:disabled {
|
||||
|
||||
@@ -116,10 +116,10 @@ createKeyBtn.addEventListener("click", async () => {
|
||||
<p>Make sure to copy your secret key — it's required to validate tokens and you won't be able to see it again.</p>
|
||||
<div class="key-creation-form">
|
||||
<label for="public-key">Key ID</label>
|
||||
<input type="text" id="public-key" value="${publicKey}" readonly style="opacity: 1"/>
|
||||
<input type="text" id="public-key" value="${publicKey}" readonly style="opacity: 1;user-select: all;"/>
|
||||
</div><div class="key-creation-form">
|
||||
<label for="private-key">Secret key</label>
|
||||
<input type="text" id="private-key" value="${privateKey}" readonly style="opacity: 1"/>
|
||||
<input type="text" id="private-key" value="${privateKey}" readonly style="opacity: 1;user-select: all;"/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -314,10 +314,10 @@ const reloadKeysList = async () => {
|
||||
<p>Make sure to copy your new secret key — it's required to validate tokens and you won't be able to see it again.</p>
|
||||
<div class="key-creation-form">
|
||||
<label for="public-key">Key ID</label>
|
||||
<input type="text" id="public-key" value="${key.publicKey}" readonly style="opacity: 1"/>
|
||||
<input type="text" id="public-key" value="${key.publicKey}" readonly style="opacity: 1;user-select: all;"/>
|
||||
</div><div class="key-creation-form">
|
||||
<label for="private-key">Secret key</label>
|
||||
<input type="text" id="private-key" value="${privateKey}" readonly style="opacity: 1"/>
|
||||
<input type="text" id="private-key" value="${privateKey}" readonly style="opacity: 1;user-select: all;"/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -162,7 +162,7 @@
|
||||
placeholder="••••••••••••••"
|
||||
class="password"
|
||||
/>
|
||||
<button title="Show/hide password" class="showhide-password">
|
||||
<button title="Show/hide password" class="showhide-password" type="button">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
|
||||
@@ -93,7 +93,7 @@ const auth = new Elysia({
|
||||
rateLimit({
|
||||
scoping: "scoped",
|
||||
count: 10,
|
||||
duration: "15s",
|
||||
duration: 15000,
|
||||
})
|
||||
)
|
||||
.post("/", async ({ body, cookie, set }) => {
|
||||
|
||||
Reference in New Issue
Block a user