Skip to content

Commit 3a521a3

Browse files
committed
standalone@2.0.14: experimental turso support with DB_CONNECTOR=turso
1 parent a7ad6ed commit 3a521a3

File tree

7 files changed

+756
-721
lines changed

7 files changed

+756
-721
lines changed

standalone/bun.lock

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

standalone/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "cap-standalone",
3-
"version": "2.0.13",
3+
"version": "2.0.14",
44
"scripts": {
55
"test": "echo \"Error: no test specified\" && exit 1",
66
"dev": "bun run --watch src/index.js",
@@ -49,6 +49,7 @@
4949
"@elysiajs/cors": "^1.4.0",
5050
"@elysiajs/static": "^1.4.4",
5151
"@elysiajs/swagger": "^1.3.1",
52+
"@tursodatabase/database": "^0.2.2",
5253
"elysia": "^1.4.12",
5354
"elysia-rate-limit": "4.4.0"
5455
},

standalone/src/auth.js

Lines changed: 125 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -6,147 +6,140 @@ import { ratelimitGenerator } from "./ratelimit.js";
66

77
const { ADMIN_KEY } = process.env;
88

9-
const loginQuery = db.query(`
9+
const loginQuery = db.prepare(`
1010
INSERT INTO sessions (token, created, expires)
11-
VALUES ($token, $created, $expires)
11+
VALUES (?, ?, ?)
1212
`);
13-
const getValidTokenQuery = db.query(`
14-
SELECT * FROM sessions WHERE token = $token AND expires > $now LIMIT 1
13+
const getValidTokenQuery = db.prepare(`
14+
SELECT * FROM sessions WHERE token = ? AND expires > ? LIMIT 1
1515
`);
1616

1717
if (!ADMIN_KEY) throw new Error("auth: Admin key missing. Please add one");
1818
if (ADMIN_KEY.length < 30)
19-
throw new Error(
20-
"auth: Admin key too short. Please use one that's at least 30 characters",
21-
);
19+
throw new Error(
20+
"auth: Admin key too short. Please use one that's at least 30 characters"
21+
);
2222

2323
export const auth = new Elysia({
24-
prefix: "/auth",
24+
prefix: "/auth",
2525
})
26-
.use(
27-
rateLimit({
28-
duration: 30_000,
29-
max: 20_000,
30-
scoping: "scoped",
31-
generator: ratelimitGenerator,
32-
}),
33-
)
34-
.post("/login", async ({ body, set, cookie }) => {
35-
const { admin_key } = body;
36-
37-
const a = Buffer.from(admin_key, "utf8");
38-
const b = Buffer.from(ADMIN_KEY, "utf8");
39-
40-
if (!a || !b || a.length !== b.length) {
41-
set.status = 401;
42-
return { success: false };
43-
}
44-
45-
if (!timingSafeEqual(a, b)) {
46-
set.status = 401;
47-
return { success: false };
48-
}
49-
50-
if (admin_key !== ADMIN_KEY) {
51-
// as a last check, in case an attacker somehow bypasses
52-
// timingSafeEqual, we're checking AGAIN to see if the tokens
53-
// are right.
54-
55-
// yes, this is vulnerable to timing attacks, but those are
56-
// hard to execute and literally just accepting an invalid token
57-
// is worse.
58-
59-
set.status = 401;
60-
return { success: false };
61-
}
62-
63-
const session_token = randomBytes(30).toString("hex");
64-
const expires = Date.now() + 30 * 24 * 60 * 60 * 1000; // 30 days
65-
const created = Date.now();
66-
67-
const hashedToken = await Bun.password.hash(session_token);
68-
69-
loginQuery.run({
70-
$token: hashedToken,
71-
$created: created,
72-
$expires: expires,
73-
});
74-
75-
cookie.cap_authed.set({
76-
value: "yes",
77-
expires: new Date(expires),
78-
});
79-
80-
return { success: true, session_token, hashed_token: hashedToken, expires };
81-
});
26+
.use(
27+
rateLimit({
28+
duration: 30_000,
29+
max: 20_000,
30+
scoping: "scoped",
31+
generator: ratelimitGenerator,
32+
})
33+
)
34+
.post("/login", async ({ body, set, cookie }) => {
35+
const { admin_key } = body;
36+
37+
const a = Buffer.from(admin_key, "utf8");
38+
const b = Buffer.from(ADMIN_KEY, "utf8");
39+
40+
if (!a || !b || a.length !== b.length) {
41+
set.status = 401;
42+
return { success: false };
43+
}
44+
45+
if (!timingSafeEqual(a, b)) {
46+
set.status = 401;
47+
return { success: false };
48+
}
49+
50+
if (admin_key !== ADMIN_KEY) {
51+
// as a last check, in case an attacker somehow bypasses
52+
// timingSafeEqual, we're checking AGAIN to see if the tokens
53+
// are right.
54+
55+
// yes, this is vulnerable to timing attacks, but those are
56+
// hard to execute and literally just accepting an invalid token
57+
// is worse.
58+
59+
set.status = 401;
60+
return { success: false };
61+
}
62+
63+
const session_token = randomBytes(30).toString("hex");
64+
const expires = Date.now() + 30 * 24 * 60 * 60 * 1000; // 30 days
65+
const created = Date.now();
66+
67+
const hashedToken = await Bun.password.hash(session_token);
68+
69+
loginQuery.run(hashedToken, created, expires);
70+
71+
cookie.cap_authed.set({
72+
value: "yes",
73+
expires: new Date(expires),
74+
});
75+
76+
return { success: true, session_token, hashed_token: hashedToken, expires };
77+
});
8278

8379
export const authBeforeHandle = async ({ set, headers }) => {
84-
const { authorization } = headers;
85-
86-
set.headers["X-Content-Type-Options"] = "nosniff";
87-
set.headers["X-Frame-Options"] = "DENY";
88-
set.headers["X-XSS-Protection"] = "1; mode=block";
89-
90-
if (authorization?.startsWith("Bot ")) {
91-
const botToken = authorization.replace("Bot ", "").trim();
92-
const [id, token] = botToken.split("_");
93-
94-
if (!id || !token) {
95-
set.status = 401;
96-
return { success: false, error: "Unauthorized. Invalid bot token." };
97-
}
98-
99-
const apiKey = db.query(`SELECT * FROM api_keys WHERE id = $id`).get({
100-
$id: id,
101-
});
102-
103-
if (!apiKey) {
104-
set.status = 401;
105-
return {
106-
success: false,
107-
error: "Unauthorized. Deleted or non-existent bot token.",
108-
};
109-
}
110-
111-
if (!(await Bun.password.verify(token, apiKey.tokenHash))) {
112-
set.status = 401;
113-
return { success: false, error: "Unauthorized. Invalid bot token." };
114-
}
115-
116-
return;
117-
}
118-
119-
if (!authorization || !authorization.startsWith("Bearer ")) {
120-
set.status = 401;
121-
return {
122-
success: false,
123-
error:
124-
"Unauthorized. An API key or session token is required to use this endpoint.",
125-
};
126-
}
127-
128-
const { token, hash } = JSON.parse(
129-
atob(authorization.replace("Bearer ", "").trim()),
130-
);
131-
132-
const validToken = getValidTokenQuery.get({
133-
$token: hash,
134-
$now: Date.now(),
135-
});
136-
137-
if (!validToken) {
138-
set.status = 401;
139-
return {
140-
success: false,
141-
error: "Unauthorized. An invalid session token was used.",
142-
};
143-
}
144-
145-
if (!(await Bun.password.verify(token, validToken.token))) {
146-
set.status = 401;
147-
return {
148-
success: false,
149-
error: "Unauthorized. An invalid session token was used.",
150-
};
151-
}
80+
const { authorization } = headers;
81+
82+
set.headers["X-Content-Type-Options"] = "nosniff";
83+
set.headers["X-Frame-Options"] = "DENY";
84+
set.headers["X-XSS-Protection"] = "1; mode=block";
85+
86+
if (authorization?.startsWith("Bot ")) {
87+
const botToken = authorization.replace("Bot ", "").trim();
88+
const [id, token] = botToken.split("_");
89+
90+
if (!id || !token) {
91+
set.status = 401;
92+
return { success: false, error: "Unauthorized. Invalid bot token." };
93+
}
94+
95+
const apiKey = await db
96+
.prepare(`SELECT * FROM api_keys WHERE id = ?`)
97+
.get(id);
98+
99+
if (!apiKey || !apiKey.tokenHash) {
100+
set.status = 401;
101+
return {
102+
success: false,
103+
error: "Unauthorized. Deleted or non-existent bot token.",
104+
};
105+
}
106+
107+
if (!(await Bun.password.verify(token, apiKey.tokenHash))) {
108+
set.status = 401;
109+
return { success: false, error: "Unauthorized. Invalid bot token." };
110+
}
111+
112+
return;
113+
}
114+
115+
if (!authorization || !authorization.startsWith("Bearer ")) {
116+
set.status = 401;
117+
return {
118+
success: false,
119+
error:
120+
"Unauthorized. An API key or session token is required to use this endpoint.",
121+
};
122+
}
123+
124+
const { token, hash } = JSON.parse(
125+
atob(authorization.replace("Bearer ", "").trim())
126+
);
127+
128+
const validToken = await getValidTokenQuery.get(hash, Date.now());
129+
130+
if (!validToken) {
131+
set.status = 401;
132+
return {
133+
success: false,
134+
error: "Unauthorized. An invalid session token was used.",
135+
};
136+
}
137+
138+
if (!(await Bun.password.verify(token, validToken.token))) {
139+
set.status = 401;
140+
return {
141+
success: false,
142+
error: "Unauthorized. An invalid session token was used.",
143+
};
144+
}
152145
};

standalone/src/cap.js

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,27 +6,27 @@ import { rateLimit } from "elysia-rate-limit";
66
import { db } from "./db.js";
77
import { ratelimitGenerator } from "./ratelimit.js";
88

9-
const getSitekeyConfigQuery = db.query(
9+
const getSitekeyConfigQuery = db.prepare(
1010
`SELECT (config) FROM keys WHERE siteKey = ?`,
1111
);
1212

13-
const insertChallengeQuery = db.query(`
13+
const insertChallengeQuery = db.prepare(`
1414
INSERT INTO challenges (siteKey, token, data, expires)
1515
VALUES (?, ?, ?, ?)
1616
`);
17-
const getChallengeQuery = db.query(`
17+
const getChallengeQuery = db.prepare(`
1818
SELECT * FROM challenges WHERE siteKey = ? AND token = ?
1919
`);
20-
const deleteChallengeQuery = db.query(`
20+
const deleteChallengeQuery = db.prepare(`
2121
DELETE FROM challenges WHERE siteKey = ? AND token = ?
2222
`);
2323

24-
const insertTokenQuery = db.query(`
24+
const insertTokenQuery = db.prepare(`
2525
INSERT INTO tokens (siteKey, token, expires)
2626
VALUES (?, ?, ?)
2727
`);
2828

29-
const upsertSolutionQuery = db.query(`
29+
const upsertSolutionQuery = db.prepare(`
3030
INSERT INTO solutions (siteKey, bucket, count)
3131
VALUES (?, ?, 1)
3232
ON CONFLICT (siteKey, bucket)
@@ -56,7 +56,7 @@ export const capServer = new Elysia({
5656
const cap = new Cap({
5757
noFSState: true,
5858
});
59-
const _keyConfig = getSitekeyConfigQuery.get(params.siteKey);
59+
const _keyConfig = await getSitekeyConfigQuery.get(params.siteKey);
6060

6161
if (!_keyConfig) {
6262
set.status = 404;
@@ -81,7 +81,7 @@ export const capServer = new Elysia({
8181
return challenge;
8282
})
8383
.post("/:siteKey/redeem", async ({ body, set, params }) => {
84-
const challenge = getChallengeQuery.get(params.siteKey, body.token);
84+
const challenge = await getChallengeQuery.get(params.siteKey, body.token);
8585

8686
try {
8787
deleteChallengeQuery.run(params.siteKey, body.token);

0 commit comments

Comments
 (0)