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>
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user