Files
memind/page-data-session-store.mjs
T
john 8d01553469 feat(page-data): add private and public dataset API (phase 1-3)
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>
2026-07-08 12:44:23 +08:00

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);
}