server(4.0.1): storage hooks: move from listExpired to deleteExpired
this should result in much faster cleanup
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
import fs from "node:fs/promises";
|
||||
import { Elysia, file } from "elysia";
|
||||
import Cap from "../../server/index.js";
|
||||
|
||||
const cap = new Cap({
|
||||
tokens_store_path: ".data/tokensList.json",
|
||||
});
|
||||
|
||||
const app = new Elysia();
|
||||
|
||||
app.get("/", () => file("./index.html"));
|
||||
|
||||
app.get("/cap.js", async ({ set }) => {
|
||||
// in the newest version, the worker is injected into the main file
|
||||
// by a build script. since we don't have a build script here,
|
||||
// we'll need to run a minimal build ourselves.
|
||||
|
||||
const main = await fs.readFile("../../widget/src/src/cap.js", "utf-8");
|
||||
const worker = await fs.readFile("../../widget/src/src/worker.js", "utf-8");
|
||||
|
||||
const bundle = main.replace("%%workerScript%%", worker);
|
||||
|
||||
set.headers = {
|
||||
"Content-Type": "application/javascript",
|
||||
};
|
||||
|
||||
return bundle;
|
||||
});
|
||||
|
||||
app.get("/cap-floating.js", () => file("../../widget/src/src/cap-floating.js"));
|
||||
|
||||
app.post("/api/challenge", () => cap.createChallenge());
|
||||
|
||||
app.post("/api/redeem", async ({ body, set }) => {
|
||||
const { token, solutions } = body;
|
||||
if (!token || !solutions) {
|
||||
set.status = 400;
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
const answer = await cap.redeemChallenge({ token, solutions });
|
||||
|
||||
console.log("new challenge redeemed", {
|
||||
...answer,
|
||||
isValid: (await cap.validateToken(answer.token)).success,
|
||||
});
|
||||
|
||||
return answer;
|
||||
});
|
||||
|
||||
app.listen(3000);
|
||||
console.log("Server is running on http://localhost:3000");
|
||||
+86
-22
@@ -1,9 +1,73 @@
|
||||
import { Database } from "bun:sqlite";
|
||||
import fs from "node:fs/promises";
|
||||
import { Elysia, file } from "elysia";
|
||||
import Cap from "../../server/index.js";
|
||||
|
||||
const db = new Database("./.data/cap.db");
|
||||
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS challenges (
|
||||
token TEXT PRIMARY KEY,
|
||||
data TEXT NOT NULL,
|
||||
expires INTEGER NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS tokens (
|
||||
key TEXT PRIMARY KEY,
|
||||
expires INTEGER NOT NULL
|
||||
);
|
||||
`);
|
||||
|
||||
const cap = new Cap({
|
||||
tokens_store_path: ".data/tokensList.json",
|
||||
storage: {
|
||||
challenges: {
|
||||
store: async (token, challengeData) => {
|
||||
db.prepare(
|
||||
"INSERT OR REPLACE INTO challenges (token, data, expires) VALUES (?, ?, ?)",
|
||||
).run(
|
||||
token,
|
||||
JSON.stringify(challengeData.challenge),
|
||||
challengeData.expires,
|
||||
);
|
||||
},
|
||||
read: async (token) => {
|
||||
const row = db
|
||||
.prepare(
|
||||
"SELECT data, expires FROM challenges WHERE token = ? AND expires > ?",
|
||||
)
|
||||
.get(token, Date.now());
|
||||
|
||||
return row
|
||||
? { challenge: JSON.parse(row.data), expires: row.expires }
|
||||
: null;
|
||||
},
|
||||
delete: async (token) => {
|
||||
db.prepare("DELETE FROM challenges WHERE token = ?").run(token);
|
||||
},
|
||||
deleteExpired: async () => {
|
||||
db.prepare("DELETE FROM challenges WHERE expires <= ?").run(Date.now());
|
||||
},
|
||||
},
|
||||
tokens: {
|
||||
store: async (tokenKey, expires) => {
|
||||
db.prepare(
|
||||
"INSERT OR REPLACE INTO tokens (key, expires) VALUES (?, ?)",
|
||||
).run(tokenKey, expires);
|
||||
},
|
||||
get: async (tokenKey) => {
|
||||
const row = db
|
||||
.prepare("SELECT expires FROM tokens WHERE key = ? AND expires > ?")
|
||||
.get(tokenKey, Date.now());
|
||||
|
||||
return row ? row.expires : null;
|
||||
},
|
||||
delete: async (tokenKey) => {
|
||||
db.prepare("DELETE FROM tokens WHERE key = ?").run(tokenKey);
|
||||
},
|
||||
deleteExpired: async () => {
|
||||
db.prepare("DELETE FROM tokens WHERE expires <= ?").run(Date.now());
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const app = new Elysia();
|
||||
@@ -11,20 +75,20 @@ const app = new Elysia();
|
||||
app.get("/", () => file("./index.html"));
|
||||
|
||||
app.get("/cap.js", async ({ set }) => {
|
||||
// in the newest version, the worker is injected into the main file
|
||||
// by a build script. since we don't have a build script here,
|
||||
// we'll need to run a minimal build ourselves.
|
||||
// in the newest version, the worker is injected into the main file
|
||||
// by a build script. since we don't have a build script here,
|
||||
// we'll need to run a minimal build ourselves.
|
||||
|
||||
const main = await fs.readFile("../../widget/src/src/cap.js", "utf-8");
|
||||
const worker = await fs.readFile("../../widget/src/src/worker.js", "utf-8");
|
||||
const main = await fs.readFile("../../widget/src/src/cap.js", "utf-8");
|
||||
const worker = await fs.readFile("../../widget/src/src/worker.js", "utf-8");
|
||||
|
||||
const bundle = main.replace("%%workerScript%%", worker);
|
||||
const bundle = main.replace("%%workerScript%%", worker);
|
||||
|
||||
set.headers = {
|
||||
"Content-Type": "application/javascript",
|
||||
};
|
||||
set.headers = {
|
||||
"Content-Type": "application/javascript",
|
||||
};
|
||||
|
||||
return bundle;
|
||||
return bundle;
|
||||
});
|
||||
|
||||
app.get("/cap-floating.js", () => file("../../widget/src/src/cap-floating.js"));
|
||||
@@ -32,20 +96,20 @@ app.get("/cap-floating.js", () => file("../../widget/src/src/cap-floating.js"));
|
||||
app.post("/api/challenge", () => cap.createChallenge());
|
||||
|
||||
app.post("/api/redeem", async ({ body, set }) => {
|
||||
const { token, solutions } = body;
|
||||
if (!token || !solutions) {
|
||||
set.status = 400;
|
||||
return { success: false };
|
||||
}
|
||||
const { token, solutions } = body;
|
||||
if (!token || !solutions) {
|
||||
set.status = 400;
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
const answer = await cap.redeemChallenge({ token, solutions });
|
||||
const answer = await cap.redeemChallenge({ token, solutions });
|
||||
|
||||
console.log("new challenge redeemed", {
|
||||
...answer,
|
||||
isValid: (await cap.validateToken(answer.token)).success,
|
||||
});
|
||||
console.log("new challenge redeemed", {
|
||||
...answer,
|
||||
isValid: (await cap.validateToken(answer.token)).success,
|
||||
});
|
||||
|
||||
return answer;
|
||||
return answer;
|
||||
});
|
||||
|
||||
app.listen(3000);
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
import { Database } from "bun:sqlite";
|
||||
import fs from "node:fs/promises";
|
||||
import { Elysia, file } from "elysia";
|
||||
import Cap from "../../server/index.js";
|
||||
|
||||
const db = new Database("./.data/cap.db");
|
||||
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS challenges (
|
||||
token TEXT PRIMARY KEY,
|
||||
data TEXT NOT NULL,
|
||||
expires INTEGER NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS tokens (
|
||||
key TEXT PRIMARY KEY,
|
||||
expires INTEGER NOT NULL
|
||||
);
|
||||
`);
|
||||
|
||||
const cap = new Cap({
|
||||
storage: {
|
||||
challenges: {
|
||||
store: async (token, challengeData) => {
|
||||
db.prepare(
|
||||
"INSERT OR REPLACE INTO challenges (token, data, expires) VALUES (?, ?, ?)",
|
||||
).run(
|
||||
token,
|
||||
JSON.stringify(challengeData.challenge),
|
||||
challengeData.expires,
|
||||
);
|
||||
},
|
||||
read: async (token) => {
|
||||
const row = db
|
||||
.prepare(
|
||||
"SELECT data, expires FROM challenges WHERE token = ? AND expires > ?",
|
||||
)
|
||||
.get(token, Date.now());
|
||||
|
||||
return row
|
||||
? { challenge: JSON.parse(row.data), expires: row.expires }
|
||||
: null;
|
||||
},
|
||||
delete: async (token) => {
|
||||
db.prepare("DELETE FROM challenges WHERE token = ?").run(token);
|
||||
},
|
||||
listExpired: async () => {
|
||||
const rows = db
|
||||
.prepare("SELECT token FROM challenges WHERE expires <= ?")
|
||||
.all(Date.now());
|
||||
|
||||
return rows.map((row) => row.token);
|
||||
},
|
||||
},
|
||||
tokens: {
|
||||
store: async (tokenKey, expires) => {
|
||||
db.prepare(
|
||||
"INSERT OR REPLACE INTO tokens (key, expires) VALUES (?, ?)",
|
||||
).run(tokenKey, expires);
|
||||
},
|
||||
get: async (tokenKey) => {
|
||||
const row = db
|
||||
.prepare("SELECT expires FROM tokens WHERE key = ? AND expires > ?")
|
||||
.get(tokenKey, Date.now());
|
||||
|
||||
return row ? row.expires : null;
|
||||
},
|
||||
delete: async (tokenKey) => {
|
||||
db.prepare("DELETE FROM tokens WHERE key = ?").run(tokenKey);
|
||||
},
|
||||
listExpired: async () => {
|
||||
const rows = db
|
||||
.prepare("SELECT key FROM tokens WHERE expires <= ?")
|
||||
.all(Date.now());
|
||||
|
||||
return rows.map((row) => row.key);
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const app = new Elysia();
|
||||
|
||||
app.get("/", () => file("./index.html"));
|
||||
|
||||
app.get("/cap.js", async ({ set }) => {
|
||||
// in the newest version, the worker is injected into the main file
|
||||
// by a build script. since we don't have a build script here,
|
||||
// we'll need to run a minimal build ourselves.
|
||||
|
||||
const main = await fs.readFile("../../widget/src/src/cap.js", "utf-8");
|
||||
const worker = await fs.readFile("../../widget/src/src/worker.js", "utf-8");
|
||||
|
||||
const bundle = main.replace("%%workerScript%%", worker);
|
||||
|
||||
set.headers = {
|
||||
"Content-Type": "application/javascript",
|
||||
};
|
||||
|
||||
return bundle;
|
||||
});
|
||||
|
||||
app.get("/cap-floating.js", () => file("../../widget/src/src/cap-floating.js"));
|
||||
|
||||
app.post("/api/challenge", () => cap.createChallenge());
|
||||
|
||||
app.post("/api/redeem", async ({ body, set }) => {
|
||||
const { token, solutions } = body;
|
||||
if (!token || !solutions) {
|
||||
set.status = 400;
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
const answer = await cap.redeemChallenge({ token, solutions });
|
||||
|
||||
console.log("new challenge redeemed", {
|
||||
...answer,
|
||||
isValid: (await cap.validateToken(answer.token)).success,
|
||||
});
|
||||
|
||||
return answer;
|
||||
});
|
||||
|
||||
app.listen(3000);
|
||||
console.log("Server is running on http://localhost:3000");
|
||||
+1
-3
@@ -73,8 +73,6 @@ You can choose between using the Standalone server or implementing your own serv
|
||||
|
||||
- For most use cases, we recommend using the **[Standalone server](./standalone/index.md)** as it provides a complete solution with built-in features like token storage, ratelimiting, analytics, and a pretty nice dashboard. However, it does require docker and exposing the server port.
|
||||
|
||||
- However, if you prefer to implement your own server, you can use the `@cap.js/server` package. This package provides the necessary methods to create and validate challenges, as well as redeem solutions, but it only works with a JavaScript backend.
|
||||
|
||||
It also gives you a small, simple JSON token storage, but you'll probably want to use a more robust solution like SQLite or Redis, but don't worry, we'll tell you everything about that [on the server guide](./server.md).
|
||||
- However, if you prefer to implement your own server, you can use the `@cap.js/server` package. This package provides the necessary methods to create and validate challenges, as well as redeem solutions, but it only works with a JavaScript backend and you'll have to add your own db. [See the server guide](./server.md) for more details.
|
||||
|
||||
- If you would like the simplicity of the server package but with another language, check out the [community packages](./community.md)
|
||||
|
||||
+6
-14
@@ -62,12 +62,8 @@ const cap = new Cap({
|
||||
delete: async (token) => {
|
||||
db.prepare("DELETE FROM challenges WHERE token = ?").run(token);
|
||||
},
|
||||
listExpired: async () => {
|
||||
const rows = db
|
||||
.prepare("SELECT token FROM challenges WHERE expires <= ?")
|
||||
.all(Date.now());
|
||||
|
||||
return rows.map((row) => row.token);
|
||||
deleteExpired: async () => {
|
||||
db.prepare("DELETE FROM challenges WHERE expires <= ?").run(Date.now());
|
||||
},
|
||||
},
|
||||
tokens: {
|
||||
@@ -86,12 +82,8 @@ const cap = new Cap({
|
||||
delete: async (tokenKey) => {
|
||||
db.prepare("DELETE FROM tokens WHERE key = ?").run(tokenKey);
|
||||
},
|
||||
listExpired: async () => {
|
||||
const rows = db
|
||||
.prepare("SELECT key FROM tokens WHERE expires <= ?")
|
||||
.all(Date.now());
|
||||
|
||||
return rows.map((row) => row.key);
|
||||
deleteExpired: async () => {
|
||||
db.prepare("DELETE FROM tokens WHERE expires <= ?").run(Date.now());
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -201,13 +193,13 @@ if (!success) throw new Error("invalid cap token");
|
||||
"store": "async (token, challengeData) => {}",
|
||||
"read": "async (token) => {}",
|
||||
"delete": "async (token) => {}",
|
||||
"listExpired": "async () => []"
|
||||
"deleteExpired": "async () => {}"
|
||||
},
|
||||
"tokens": {
|
||||
"store": "async (tokenKey, expires) => {}",
|
||||
"get": "async (tokenKey) => {}",
|
||||
"delete": "async (tokenKey) => {}",
|
||||
"listExpired": "async () => []"
|
||||
"deleteExpired": "async () => {}"
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
Vendored
+4
-4
@@ -191,9 +191,9 @@ type ChallengeStorage = {
|
||||
*/
|
||||
delete: (arg0: string) => Promise<void>;
|
||||
/**
|
||||
* - List expired challenge tokens
|
||||
* - Delete expired challenge tokens
|
||||
*/
|
||||
listExpired: () => Promise<string[]>;
|
||||
deleteExpired: () => Promise<string[]>;
|
||||
};
|
||||
type TokenStorage = {
|
||||
/**
|
||||
@@ -209,9 +209,9 @@ type TokenStorage = {
|
||||
*/
|
||||
delete: (arg0: string) => Promise<void>;
|
||||
/**
|
||||
* - List expired token keys
|
||||
* - Delete expired token keys
|
||||
*/
|
||||
listExpired: () => Promise<string[]>;
|
||||
deleteExpired: () => Promise<string[]>;
|
||||
};
|
||||
type StorageHooks = {
|
||||
/**
|
||||
|
||||
+8
-21
@@ -52,7 +52,7 @@
|
||||
* @property {function(string, ChallengeData): Promise<void>} store - Store challenge data
|
||||
* @property {function(string): Promise<ChallengeData|null>} read - Retrieve challenge data
|
||||
* @property {function(string): Promise<void>} delete - Delete challenge data
|
||||
* @property {function(): Promise<string[]>} listExpired - List expired challenge tokens
|
||||
* @property {function(): Promise<string[]>} deleteExpired - Delete expired challenge tokens
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -60,7 +60,7 @@
|
||||
* @property {function(string, number): Promise<void>} store - Store token with expiration
|
||||
* @property {function(string): Promise<number|null>} get - Retrieve token expiration
|
||||
* @property {function(string): Promise<void>} delete - Delete token
|
||||
* @property {function(): Promise<string[]>} listExpired - List expired token keys
|
||||
* @property {function(): Promise<string[]>} deleteExpired - Delete expired token keys
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -441,15 +441,8 @@ class Cap extends EventEmitter {
|
||||
const now = Date.now();
|
||||
let tokensChanged = false;
|
||||
|
||||
if (this.config.storage?.challenges?.listExpired) {
|
||||
const expiredChallenges =
|
||||
await this.config.storage.challenges.listExpired();
|
||||
|
||||
await Promise.all(
|
||||
expiredChallenges.map(async (token) => {
|
||||
await this._deleteChallenge(token);
|
||||
}),
|
||||
);
|
||||
if (this.config.storage?.challenges?.deleteExpired) {
|
||||
await this.config.storage.challenges.deleteExpired();
|
||||
} else if (!this.config.storage?.challenges) {
|
||||
const expired = Object.entries(this.config.state.challengesList)
|
||||
.filter(([_, v]) => v.expires < now)
|
||||
@@ -462,18 +455,12 @@ class Cap extends EventEmitter {
|
||||
);
|
||||
} else {
|
||||
console.warn(
|
||||
"[cap] challenge storage hooks provided but no listExpired, couldn't delete expired challenges",
|
||||
"[cap] challenge storage hooks provided but no deleteExpired, couldn't delete expired challenges",
|
||||
);
|
||||
}
|
||||
|
||||
if (this.config.storage?.tokens?.listExpired) {
|
||||
const expiredTokens = await this.config.storage.tokens.listExpired();
|
||||
await Promise.all(
|
||||
expiredTokens.map(async (tokenKey) => {
|
||||
await this._deleteToken(tokenKey);
|
||||
tokensChanged = true;
|
||||
}),
|
||||
);
|
||||
if (this.config.storage?.tokens?.deleteExpired) {
|
||||
await this.config.storage.tokens.deleteExpired();
|
||||
} else if (!this.config.storage?.tokens) {
|
||||
for (const k in this.config.state.tokensList) {
|
||||
if (this.config.state.tokensList[k] < now) {
|
||||
@@ -483,7 +470,7 @@ class Cap extends EventEmitter {
|
||||
}
|
||||
} else {
|
||||
console.warn(
|
||||
"[cap] token storage hooks provided but no listExpired, couldn't delete expired tokens",
|
||||
"[cap] token storage hooks provided but no deleteExpired, couldn't delete expired tokens",
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@cap.js/server",
|
||||
"version": "3.0.1",
|
||||
"version": "4.0.1",
|
||||
"author": "Tiago",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -54,7 +54,7 @@
|
||||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"npm:publish": "bun publish --access public"
|
||||
"publish": "bun publish --access public"
|
||||
},
|
||||
"type": "commonjs",
|
||||
"dependencies": {
|
||||
|
||||
Reference in New Issue
Block a user