8d01553469
Extract UserDataSpaceService for shared SQLite access, wire logged-in Page Data routes, and add public insert plus password-token read/update/delete with policy storage, rate limits, and regression tests. Co-authored-by: Cursor <cursoragent@cursor.com>
83 lines
2.2 KiB
JavaScript
83 lines
2.2 KiB
JavaScript
import crypto from 'node:crypto';
|
|
|
|
export function createPageDataSessionStore(options = {}) {
|
|
const ttlMs = Number(options.ttlMs ?? 30 * 60 * 1000);
|
|
const sessions = new Map();
|
|
|
|
function hashToken(token) {
|
|
return crypto.createHash('sha256').update(String(token)).digest('hex');
|
|
}
|
|
|
|
function issue({ pageId, ownerUserId, publicationId, accessMode }) {
|
|
const token = crypto.randomBytes(32).toString('base64url');
|
|
const tokenHash = hashToken(token);
|
|
const expiresAt = Date.now() + ttlMs;
|
|
sessions.set(tokenHash, {
|
|
pageId,
|
|
ownerUserId,
|
|
publicationId,
|
|
accessMode,
|
|
sessionId: crypto.randomUUID(),
|
|
expiresAt,
|
|
});
|
|
return {
|
|
token,
|
|
expiresAt,
|
|
sessionId: sessions.get(tokenHash).sessionId,
|
|
};
|
|
}
|
|
|
|
function verify(token) {
|
|
const session = sessions.get(hashToken(token));
|
|
if (!session) return null;
|
|
if (session.expiresAt <= Date.now()) {
|
|
sessions.delete(hashToken(token));
|
|
return null;
|
|
}
|
|
return session;
|
|
}
|
|
|
|
function revoke(token) {
|
|
return sessions.delete(hashToken(token));
|
|
}
|
|
|
|
function clearExpired() {
|
|
const now = Date.now();
|
|
for (const [key, session] of sessions.entries()) {
|
|
if (session.expiresAt <= now) sessions.delete(key);
|
|
}
|
|
}
|
|
|
|
return { issue, verify, revoke, clearExpired };
|
|
}
|
|
|
|
export function createPageDataRateLimiter(options = {}) {
|
|
const windowMs = Number(options.windowMs ?? 60_000);
|
|
const maxRequests = Number(options.maxRequests ?? 30);
|
|
const buckets = new Map();
|
|
|
|
function check(key) {
|
|
const now = Date.now();
|
|
const bucket = buckets.get(key) ?? { count: 0, resetAt: now + windowMs };
|
|
if (now >= bucket.resetAt) {
|
|
bucket.count = 0;
|
|
bucket.resetAt = now + windowMs;
|
|
}
|
|
bucket.count += 1;
|
|
buckets.set(key, bucket);
|
|
if (bucket.count > maxRequests) {
|
|
throw Object.assign(new Error('请求过于频繁,请稍后再试'), {
|
|
code: 'rate_limited',
|
|
status: 429,
|
|
});
|
|
}
|
|
return { remaining: Math.max(0, maxRequests - bucket.count), resetAt: bucket.resetAt };
|
|
}
|
|
|
|
return { check };
|
|
}
|
|
|
|
export function hashClientMeta(value) {
|
|
return crypto.createHash('sha256').update(String(value ?? '')).digest('hex').slice(0, 16);
|
|
}
|